From 450c0270fa335cdf09e6364912b4dcd4c540c574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 26 Oct 2022 18:37:44 +0200 Subject: [PATCH 001/446] :construction: wip on maya royalrender submit plugin --- openpype/modules/royalrender/api.py | 20 ++- .../publish/submit_maya_royalrender.py | 150 ++++++++++++++++++ openpype/modules/royalrender/rr_job.py | 8 +- 3 files changed, 163 insertions(+), 15 deletions(-) create mode 100644 openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index de1dba8724..c47d50b62b 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -15,9 +15,8 @@ class Api: RR_SUBMIT_CONSOLE = 1 RR_SUBMIT_API = 2 - def __init__(self, settings, project=None): + def __init__(self, project=None): self.log = Logger.get_logger("RoyalRender") - self._settings = settings self._initialize_rr(project) def _initialize_rr(self, project=None): @@ -91,21 +90,21 @@ class Api: sys.path.append(os.path.join(self._rr_path, rr_module_path)) - def create_submission(self, jobs, submitter_attributes, file_name=None): - # type: (list[RRJob], list[SubmitterParameter], str) -> SubmitFile + @staticmethod + def create_submission(jobs, submitter_attributes): + # type: (list[RRJob], list[SubmitterParameter]) -> SubmitFile """Create jobs submission file. Args: jobs (list): List of :class:`RRJob` submitter_attributes (list): List of submitter attributes :class:`SubmitterParameter` for whole submission batch. - file_name (str), optional): File path to write data to. Returns: str: XML data of job submission files. """ - raise NotImplementedError + return SubmitFile(SubmitterParameters=submitter_attributes, Jobs=jobs) def submit_file(self, file, mode=RR_SUBMIT_CONSOLE): # type: (SubmitFile, int) -> None @@ -119,15 +118,14 @@ class Api: # self._submit_using_api(file) def _submit_using_console(self, file): - # type: (SubmitFile) -> bool + # type: (SubmitFile) -> None rr_console = os.path.join( self._get_rr_bin_path(), - "rrSubmitterconsole" + "rrSubmitterConsole" ) - if sys.platform.lower() == "darwin": - if "/bin/mac64" in rr_console: - rr_console = rr_console.replace("/bin/mac64", "/bin/mac") + if sys.platform.lower() == "darwin" and "/bin/mac64" in rr_console: + rr_console = rr_console.replace("/bin/mac64", "/bin/mac") if sys.platform.lower() == "win32": if "/bin/win64" in rr_console: diff --git a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py new file mode 100644 index 0000000000..c354cc80a0 --- /dev/null +++ b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +"""Submitting render job to RoyalRender.""" +import os +import sys +import tempfile + +from maya.OpenMaya import MGlobal # noqa +from pyblish.api import InstancePlugin, IntegratorOrder +from openpype.hosts.maya.api.lib import get_attr_in_layer +from openpype.pipeline.farm.tools import get_published_workfile_instance +from openpype.pipeline.publish import KnownPublishError +from openpype.modules.royalrender.api import Api as rr_api +from openpype.modules.royalrender.rr_job import RRJob, SubmitterParameter + + +class MayaSubmitRoyalRender(InstancePlugin): + label = "Submit to RoyalRender" + order = IntegratorOrder + 0.1 + use_published = True + + def __init__(self, *args, **kwargs): + self._instance = None + self._rrRoot = None + self.scene_path = None + self.job = None + self.submission_parameters = None + self.rr_api = None + + def get_job(self): + """Prepare job payload. + + Returns: + RRJob: RoyalRender job payload. + + """ + def get_rr_platform(): + if sys.platform.lower() in ["win32", "win64"]: + return "win" + elif sys.platform.lower() == "darwin": + return "mac" + else: + return "lx" + + expected_files = self._instance.data["expectedFiles"] + first_file = next(self._iter_expected_files(expected_files)) + output_dir = os.path.dirname(first_file) + self._instance.data["outputDir"] = output_dir + workspace = self._instance.context.data["workspaceDir"] + default_render_file = self._instance.context.data.get('project_settings') \ + .get('maya') \ + .get('RenderSettings') \ + .get('default_render_image_folder') + filename = os.path.basename(self.scene_path) + dirname = os.path.join(workspace, default_render_file) + + job = RRJob( + Software="Maya", + Renderer=self._instance.data["renderer"], + SeqStart=int(self._instance.data["frameStartHandle"]), + SeqEnd=int(self._instance.data["frameEndHandle"]), + SeqStep=int(self._instance.data["byFrameStep"]), + SeqFileOffset=0, + Version="{0:.2f}".format(MGlobal.apiVersion() / 10000), + SceneName=os.path.basename(self.scene_path), + IsActive=True, + ImageDir=dirname, + ImageFilename=filename, + ImageExtension="." + os.path.splitext(filename)[1], + ImagePreNumberLetter=".", + ImageSingleOutputFile=False, + SceneOS=get_rr_platform(), + Camera=self._instance.data["cameras"][0], + Layer=self._instance.data["layer"], + SceneDatabaseDir=workspace, + ImageFramePadding=get_attr_in_layer( + "defaultRenderGlobals.extensionPadding", + self._instance.data["layer"]), + ImageWidth=self._instance.data["resolutionWidth"], + ImageHeight=self._instance.data["resolutionHeight"] + ) + return job + + @staticmethod + def get_submission_parameters(): + return [] + + def create_file(self, name, ext, contents=None): + temp = tempfile.NamedTemporaryFile( + dir=self.tempdir, + suffix=ext, + prefix=name + '.', + delete=False, + ) + + if contents: + with open(temp.name, 'w') as f: + f.write(contents) + + return temp.name + + def process(self, instance): + """Plugin entry point.""" + self._instance = instance + context = instance.context + self.rr_api = rr_api(context.data["project"]) + + # get royalrender module + """ + try: + rr_module = context.data.get( + "openPypeModules")["royalrender"] + except AttributeError: + self.log.error("Cannot get OpenPype RoyalRender module.") + raise AssertionError("OpenPype RoyalRender module not found.") + """ + + self._rrRoot = instance.data["rrPath"] or context.data["defaultRRPath"] # noqa + if not self._rrRoot: + raise KnownPublishError( + ("Missing RoyalRender root. " + "You need to configure RoyalRender module.")) + file_path = None + if self.use_published: + file_path = get_published_workfile_instance() + + # fallback if nothing was set + if not file_path: + self.log.warning("Falling back to workfile") + file_path = context.data["currentFile"] + + self.scene_path = file_path + self.job = self.get_job() + self.log.info(self.job) + self.submission_parameters = self.get_submission_parameters() + + self.process_submission() + + def process_submission(self): + submission = rr_api.create_submission( + [self.job], + self.submission_parameters) + + self.log.debug(submission) + xml = tempfile.NamedTemporaryFile(suffix=".xml", delete=False) + with open(xml.name, "w") as f: + f.write(submission.serialize()) + + self.rr_api.submit_file(file=xml) + + diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index c660eceac7..beb8c17187 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -35,7 +35,7 @@ class RRJob: # Is the job enabled for submission? # enabled by default - IsActive = attr.ib() # type: str + IsActive = attr.ib() # type: bool # Sequence settings of this job SeqStart = attr.ib() # type: int @@ -60,7 +60,7 @@ class RRJob: # If you render a single file, e.g. Quicktime or Avi, then you have to # set this value. Videos have to be rendered at once on one client. - ImageSingleOutputFile = attr.ib(default="false") # type: str + ImageSingleOutputFile = attr.ib(default=False) # type: bool # Semi-Required (required for some render applications) # ----------------------------------------------------- @@ -169,11 +169,11 @@ class SubmitFile: # Delete submission file after processing DeleteXML = attr.ib(default=1) # type: int - # List of submitter options per job + # List of the submitter options per job. # list item must be of `SubmitterParameter` type SubmitterParameters = attr.ib(factory=list) # type: list - # List of job is submission batch. + # List of the jobs in submission batch. # list item must be of type `RRJob` Jobs = attr.ib(factory=list) # type: list From 635fe48edb835b1748daf3023bac7781f44d2f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 26 Oct 2022 18:38:11 +0200 Subject: [PATCH 002/446] :recycle: move functions to common lib --- .../deadline/abstract_submit_deadline.py | 112 +----------------- openpype/pipeline/farm/tools.py | 109 +++++++++++++++++ 2 files changed, 111 insertions(+), 110 deletions(-) create mode 100644 openpype/pipeline/farm/tools.py diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 512ff800ee..0299b3ff18 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -21,6 +21,7 @@ from openpype.pipeline.publish import ( AbstractMetaInstancePlugin, KnownPublishError ) +from openpype.pipeline.farm.tools import get_published_workfile_instance JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) @@ -424,7 +425,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): file_path = None if self.use_published: - file_path = self.from_published_scene() + file_path = get_published_workfile_instance(instance) # fallback if nothing was set if not file_path: @@ -495,96 +496,6 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): """ return [] - def from_published_scene(self, replace_in_path=True): - """Switch work scene for published scene. - - If rendering/exporting from published scenes is enabled, this will - replace paths from working scene to published scene. - - Args: - replace_in_path (bool): if True, it will try to find - old scene name in path of expected files and replace it - with name of published scene. - - Returns: - str: Published scene path. - None: if no published scene is found. - - Note: - Published scene path is actually determined from project Anatomy - as at the time this plugin is running scene can still no be - published. - - """ - - instance = self._instance - workfile_instance = self._get_workfile_instance(instance.context) - if workfile_instance is None: - return - - # determine published path from Anatomy. - template_data = workfile_instance.data.get("anatomyData") - rep = workfile_instance.data.get("representations")[0] - template_data["representation"] = rep.get("name") - template_data["ext"] = rep.get("ext") - template_data["comment"] = None - - anatomy = instance.context.data['anatomy'] - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] - file_path = os.path.normpath(template_filled) - - self.log.info("Using published scene for render {}".format(file_path)) - - if not os.path.exists(file_path): - self.log.error("published scene does not exist!") - raise - - if not replace_in_path: - return file_path - - # now we need to switch scene in expected files - # because token will now point to published - # scene file and that might differ from current one - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(file_path) - orig_scene = _clean_name(instance.context.data["currentFile"]) - expected_files = instance.data.get("expectedFiles") - - if isinstance(expected_files[0], dict): - # we have aovs and we need to iterate over them - new_exp = {} - for aov, files in expected_files[0].items(): - replaced_files = [] - for f in files: - replaced_files.append( - str(f).replace(orig_scene, new_scene) - ) - new_exp[aov] = replaced_files - # [] might be too much here, TODO - instance.data["expectedFiles"] = [new_exp] - else: - new_exp = [] - for f in expected_files: - new_exp.append( - str(f).replace(orig_scene, new_scene) - ) - instance.data["expectedFiles"] = new_exp - - metadata_folder = instance.data.get("publishRenderMetadataFolder") - if metadata_folder: - metadata_folder = metadata_folder.replace(orig_scene, - new_scene) - instance.data["publishRenderMetadataFolder"] = metadata_folder - - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) - - return file_path - def assemble_payload( self, job_info=None, plugin_info=None, aux_files=None): """Assemble payload data from its various parts. @@ -644,22 +555,3 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): self._instance.data["deadlineSubmissionJob"] = result return result["_id"] - - @staticmethod - def _get_workfile_instance(context): - """Find workfile instance in context""" - for i in context: - - is_workfile = ( - "workfile" in i.data.get("families", []) or - i.data["family"] == "workfile" - ) - if not is_workfile: - continue - - # test if there is instance of workfile waiting - # to be published. - assert i.data["publish"] is True, ( - "Workfile (scene) must be published along") - - return i diff --git a/openpype/pipeline/farm/tools.py b/openpype/pipeline/farm/tools.py new file mode 100644 index 0000000000..8cf1af399e --- /dev/null +++ b/openpype/pipeline/farm/tools.py @@ -0,0 +1,109 @@ +import os + + +def get_published_workfile_instance(context): + """Find workfile instance in context""" + for i in context: + is_workfile = ( + "workfile" in i.data.get("families", []) or + i.data["family"] == "workfile" + ) + if not is_workfile: + continue + + # test if there is instance of workfile waiting + # to be published. + if i.data["publish"] is not True: + continue + + return i + + +def from_published_scene(instance, replace_in_path=True): + """Switch work scene for published scene. + + If rendering/exporting from published scenes is enabled, this will + replace paths from working scene to published scene. + + Args: + instance (pyblish.api.Instance): Instance data to process. + replace_in_path (bool): if True, it will try to find + old scene name in path of expected files and replace it + with name of published scene. + + Returns: + str: Published scene path. + None: if no published scene is found. + + Note: + Published scene path is actually determined from project Anatomy + as at the time this plugin is running the scene can be still + un-published. + + """ + workfile_instance = get_published_workfile_instance(instance.context) + if workfile_instance is None: + return + + # determine published path from Anatomy. + template_data = workfile_instance.data.get("anatomyData") + rep = workfile_instance.data.get("representations")[0] + template_data["representation"] = rep.get("name") + template_data["ext"] = rep.get("ext") + template_data["comment"] = None + + anatomy = instance.context.data['anatomy'] + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled["publish"]["path"] + file_path = os.path.normpath(template_filled) + + self.log.info("Using published scene for render {}".format(file_path)) + + if not os.path.exists(file_path): + self.log.error("published scene does not exist!") + raise + + if not replace_in_path: + return file_path + + # now we need to switch scene in expected files + # because token will now point to published + # scene file and that might differ from current one + def _clean_name(path): + return os.path.splitext(os.path.basename(path))[0] + + new_scene = _clean_name(file_path) + orig_scene = _clean_name(instance.context.data["currentFile"]) + expected_files = instance.data.get("expectedFiles") + + if isinstance(expected_files[0], dict): + # we have aovs and we need to iterate over them + new_exp = {} + for aov, files in expected_files[0].items(): + replaced_files = [] + for f in files: + replaced_files.append( + str(f).replace(orig_scene, new_scene) + ) + new_exp[aov] = replaced_files + # [] might be too much here, TODO + instance.data["expectedFiles"] = [new_exp] + else: + new_exp = [] + for f in expected_files: + new_exp.append( + str(f).replace(orig_scene, new_scene) + ) + instance.data["expectedFiles"] = new_exp + + metadata_folder = instance.data.get("publishRenderMetadataFolder") + if metadata_folder: + metadata_folder = metadata_folder.replace(orig_scene, + new_scene) + instance.data["publishRenderMetadataFolder"] = metadata_folder + + self.log.info("Scene name was switched {} -> {}".format( + orig_scene, new_scene + )) + + return file_path From aaafcd633341e774944b7fb865acc3f790a60f70 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 11 Jan 2023 19:13:28 +0100 Subject: [PATCH 003/446] :construction: redoing publishing flow for multiple rr roots --- .../maya/plugins/create/create_render.py | 89 +++++++++++++++---- .../maya/plugins/publish/collect_render.py | 7 ++ openpype/modules/royalrender/api.py | 30 +------ .../publish/collect_default_rr_path.py | 23 ----- .../publish/collect_rr_path_from_instance.py | 16 ++-- .../publish/collect_sequences_from_job.py | 2 +- .../publish/submit_maya_royalrender.py | 51 +++++++++-- .../project_settings/royalrender.json | 3 + .../defaults/system_settings/modules.json | 6 +- 9 files changed, 138 insertions(+), 89 deletions(-) delete mode 100644 openpype/modules/royalrender/plugins/publish/collect_default_rr_path.py diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 8375149442..dedb057fb7 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -79,31 +79,58 @@ class CreateRender(plugin.Creator): if self._project_settings["maya"]["RenderSettings"]["apply_render_settings"]: # noqa lib_rendersettings.RenderSettings().set_default_renderer_settings() - # Deadline-only + # Handling farms manager = ModulesManager() deadline_settings = get_system_settings()["modules"]["deadline"] - if not deadline_settings["enabled"]: - self.deadline_servers = {} + rr_settings = get_system_settings()["modules"]["royalrender"] + + self.deadline_servers = {} + self.rr_paths = {} + + if deadline_settings["enabled"]: + self.deadline_module = manager.modules_by_name["deadline"] + try: + default_servers = deadline_settings["deadline_urls"] + project_servers = ( + self._project_settings["deadline"]["deadline_servers"] + ) + self.deadline_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + + if not self.deadline_servers: + self.deadline_servers = default_servers + + except AttributeError: + # Handle situation were we had only one url for deadline. + # get default deadline webservice url from deadline module + self.deadline_servers = self.deadline_module.deadline_urls + + # RoyalRender only + if not rr_settings["enabled"]: return - self.deadline_module = manager.modules_by_name["deadline"] + + self.rr_module = manager.modules_by_name["royalrender"] try: - default_servers = deadline_settings["deadline_urls"] - project_servers = ( - self._project_settings["deadline"]["deadline_servers"] + default_paths = rr_settings["rr_paths"] + project_paths = ( + self._project_settings["royalrender"]["rr_paths"] ) - self.deadline_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers + self.rr_paths = { + k: default_paths[k] + for k in project_paths + if k in default_paths } - if not self.deadline_servers: - self.deadline_servers = default_servers + if not self.rr_paths: + self.rr_paths = default_paths except AttributeError: - # Handle situation were we had only one url for deadline. - # get default deadline webservice url from deadline module - self.deadline_servers = self.deadline_module.deadline_urls + # Handle situation were we had only one path for royalrender. + # Get default royalrender root path from the rr module. + self.rr_paths = self.rr_module.rr_paths def process(self): """Entry point.""" @@ -139,6 +166,14 @@ class CreateRender(plugin.Creator): self._deadline_webservice_changed ]) + # add RoyalRender root path selection list + if self.rr_paths: + cmds.scriptJob( + attributeChange=[ + "{}.rrPaths".format(self.instance), + self._rr_path_changed + ]) + cmds.setAttr("{}.machineList".format(self.instance), lock=True) rs = renderSetup.instance() layers = rs.getRenderLayers() @@ -191,6 +226,18 @@ class CreateRender(plugin.Creator): attributeType="enum", enumName=":".join(sorted_pools)) + @staticmethod + def _rr_path_changed(): + """Unused callback to pull information from RR.""" + """ + _ = self.rr_paths[ + self.server_aliases[ + cmds.getAttr("{}.rrPaths".format(self.instance)) + ] + ] + """ + pass + def _create_render_settings(self): """Create instance settings.""" # get pools (slave machines of the render farm) @@ -225,15 +272,21 @@ class CreateRender(plugin.Creator): system_settings = get_system_settings()["modules"] deadline_enabled = system_settings["deadline"]["enabled"] + royalrender_enabled = system_settings["royalrender"]["enabled"] muster_enabled = system_settings["muster"]["enabled"] muster_url = system_settings["muster"]["MUSTER_REST_URL"] - if deadline_enabled and muster_enabled: + if deadline_enabled and muster_enabled and royalrender_enabled: self.log.error( - "Both Deadline and Muster are enabled. " "Cannot support both." + ("Multiple render farm support (Deadline/RoyalRender/Muster) " + "is enabled. We support only one at time.") ) raise RuntimeError("Both Deadline and Muster are enabled") + if royalrender_enabled: + self.server_aliases = list(self.rr_paths.keys()) + self.data["rrPaths"] = self.server_aliases + if deadline_enabled: self.server_aliases = list(self.deadline_servers.keys()) self.data["deadlineServers"] = self.server_aliases diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index b1ad3ca58e..efae830b64 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -328,6 +328,13 @@ class CollectMayaRender(pyblish.api.ContextPlugin): if deadline_settings["enabled"]: data["deadlineUrl"] = render_instance.data.get("deadlineUrl") + rr_settings = ( + context.data["system_settings"]["modules"]["royalrender"] + ) + if rr_settings["enabled"]: + data["rrPathName"] = render_instance.data.get("rrPathName") + self.log.info(data["rrPathName"]) + if self.sync_workfile_version: data["version"] = context.data["version"] diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index c47d50b62b..dcb518deb1 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -15,36 +15,10 @@ class Api: RR_SUBMIT_CONSOLE = 1 RR_SUBMIT_API = 2 - def __init__(self, project=None): + def __init__(self, rr_path=None): self.log = Logger.get_logger("RoyalRender") - self._initialize_rr(project) - - def _initialize_rr(self, project=None): - # type: (str) -> None - """Initialize RR Path. - - Args: - project (str, Optional): Project name to set RR api in - context. - - """ - if project: - project_settings = get_project_settings(project) - rr_path = ( - project_settings - ["royalrender"] - ["rr_paths"] - ) - else: - rr_path = ( - self._settings - ["modules"] - ["royalrender"] - ["rr_path"] - ["default"] - ) - os.environ["RR_ROOT"] = rr_path self._rr_path = rr_path + os.environ["RR_ROOT"] = rr_path def _get_rr_bin_path(self, rr_root=None): # type: (str) -> str diff --git a/openpype/modules/royalrender/plugins/publish/collect_default_rr_path.py b/openpype/modules/royalrender/plugins/publish/collect_default_rr_path.py deleted file mode 100644 index 3ce95e0c50..0000000000 --- a/openpype/modules/royalrender/plugins/publish/collect_default_rr_path.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect default Deadline server.""" -import pyblish.api - - -class CollectDefaultRRPath(pyblish.api.ContextPlugin): - """Collect default Royal Render path.""" - - order = pyblish.api.CollectorOrder - label = "Default Royal Render Path" - - def process(self, context): - try: - rr_module = context.data.get( - "openPypeModules")["royalrender"] - except AttributeError: - msg = "Cannot get OpenPype Royal Render module." - self.log.error(msg) - raise AssertionError(msg) - - # get default deadline webservice url from deadline module - self.log.debug(rr_module.rr_paths) - context.data["defaultRRPath"] = rr_module.rr_paths["default"] # noqa: E501 diff --git a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index 6a3dc276f3..187e2b9c44 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -5,19 +5,19 @@ import pyblish.api class CollectRRPathFromInstance(pyblish.api.InstancePlugin): """Collect RR Path from instance.""" - order = pyblish.api.CollectorOrder + 0.01 - label = "Royal Render Path from the Instance" + order = pyblish.api.CollectorOrder + label = "Collect Royal Render path name from the Instance" families = ["rendering"] def process(self, instance): - instance.data["rrPath"] = self._collect_rr_path(instance) + instance.data["rrPathName"] = self._collect_rr_path_name(instance) self.log.info( - "Using {} for submission.".format(instance.data["rrPath"])) + "Using '{}' for submission.".format(instance.data["rrPathName"])) @staticmethod - def _collect_rr_path(render_instance): + def _collect_rr_path_name(render_instance): # type: (pyblish.api.Instance) -> str - """Get Royal Render path from render instance.""" + """Get Royal Render pat name from render instance.""" rr_settings = ( render_instance.context.data ["system_settings"] @@ -42,8 +42,6 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): # Handle situation were we had only one url for royal render. return render_instance.context.data["defaultRRPath"] - return rr_servers[ - list(rr_servers.keys())[ + return list(rr_servers.keys())[ int(render_instance.data.get("rrPaths")) ] - ] diff --git a/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py b/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py index 65af90e8a6..4c123e4134 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py +++ b/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py @@ -71,7 +71,7 @@ class CollectSequencesFromJob(pyblish.api.ContextPlugin): """Gather file sequences from job directory. When "OPENPYPE_PUBLISH_DATA" environment variable is set these paths - (folders or .json files) are parsed for image sequences. Otherwise the + (folders or .json files) are parsed for image sequences. Otherwise, the current working directory is searched for file sequences. """ diff --git a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py index f1ba5e62ef..ef98bcf74c 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py @@ -3,9 +3,10 @@ import os import sys import tempfile +import platform from maya.OpenMaya import MGlobal # noqa -from pyblish.api import InstancePlugin, IntegratorOrder +from pyblish.api import InstancePlugin, IntegratorOrder, Context from openpype.hosts.maya.api.lib import get_attr_in_layer from openpype.pipeline.publish.lib import get_published_workfile_instance from openpype.pipeline.publish import KnownPublishError @@ -16,6 +17,8 @@ from openpype.modules.royalrender.rr_job import RRJob, SubmitterParameter class MayaSubmitRoyalRender(InstancePlugin): label = "Submit to RoyalRender" order = IntegratorOrder + 0.1 + families = ["renderlayer"] + targets = ["local"] use_published = True def __init__(self, *args, **kwargs): @@ -102,7 +105,16 @@ class MayaSubmitRoyalRender(InstancePlugin): """Plugin entry point.""" self._instance = instance context = instance.context - self.rr_api = rr_api(context.data["project"]) + from pprint import pformat + + self._rr_root = self._resolve_rr_path(context, instance.data.get("rrPathName")) # noqa + self.log.debug(self._rr_root) + if not self._rr_root: + raise KnownPublishError( + ("Missing RoyalRender root. " + "You need to configure RoyalRender module.")) + + self.rr_api = rr_api(self._rr_root) # get royalrender module """ @@ -114,11 +126,7 @@ class MayaSubmitRoyalRender(InstancePlugin): raise AssertionError("OpenPype RoyalRender module not found.") """ - self._rrRoot = instance.data["rrPath"] or context.data["defaultRRPath"] # noqa - if not self._rrRoot: - raise KnownPublishError( - ("Missing RoyalRender root. " - "You need to configure RoyalRender module.")) + file_path = None if self.use_published: file_path = get_published_workfile_instance() @@ -147,4 +155,33 @@ class MayaSubmitRoyalRender(InstancePlugin): self.rr_api.submit_file(file=xml) + @staticmethod + def _resolve_rr_path(context, rr_path_name): + # type: (Context, str) -> str + rr_settings = ( + context.data + ["system_settings"] + ["modules"] + ["royalrender"] + ) + try: + default_servers = rr_settings["rr_paths"] + project_servers = ( + context.data + ["project_settings"] + ["royalrender"] + ["rr_paths"] + ) + rr_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + + except (AttributeError, KeyError): + # Handle situation were we had only one url for royal render. + return context.data["defaultRRPath"][platform.system().lower()] + + return rr_servers[rr_path_name][platform.system().lower()] + diff --git a/openpype/settings/defaults/project_settings/royalrender.json b/openpype/settings/defaults/project_settings/royalrender.json index be267b11d8..dc7d1574a1 100644 --- a/openpype/settings/defaults/project_settings/royalrender.json +++ b/openpype/settings/defaults/project_settings/royalrender.json @@ -1,4 +1,7 @@ { + "rr_paths": [ + "default" + ], "publish": { "CollectSequencesFromJob": { "review": true diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index c84d23d3fc..b5f7784663 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -185,9 +185,9 @@ "enabled": false, "rr_paths": { "default": { - "windows": "", - "darwin": "", - "linux": "" + "windows": "C:\\RR8", + "darwin": "/Volumes/share/RR8", + "linux": "/mnt/studio/RR8" } } }, From 0eb7da3b93b7e54f9461575751c87672a2fb8982 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 11 Jan 2023 19:13:54 +0100 Subject: [PATCH 004/446] :recycle: optimizing enum classes --- openpype/settings/entities/__init__.py | 4 +- openpype/settings/entities/enum_entity.py | 87 ++++++++++--------- .../schema_project_royalrender.json | 6 ++ 3 files changed, 56 insertions(+), 41 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index b2cb2204f4..dab17c84a3 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -107,7 +107,8 @@ from .enum_entity import ( TaskTypeEnumEntity, DeadlineUrlEnumEntity, AnatomyTemplatesEnumEntity, - ShotgridUrlEnumEntity + ShotgridUrlEnumEntity, + RoyalRenderRootEnumEntity ) from .list_entity import ListEntity @@ -173,6 +174,7 @@ __all__ = ( "TaskTypeEnumEntity", "DeadlineUrlEnumEntity", "ShotgridUrlEnumEntity", + "RoyalRenderRootEnumEntity", "AnatomyTemplatesEnumEntity", "ListEntity", diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index c0c103ea10..9f9ae93026 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -1,3 +1,5 @@ +import abc +import six import copy from .input_entities import InputEntity from .exceptions import EntitySchemaError @@ -476,8 +478,9 @@ class TaskTypeEnumEntity(BaseEnumEntity): self.set(value_on_not_set) -class DeadlineUrlEnumEntity(BaseEnumEntity): - schema_types = ["deadline_url-enum"] +@six.add_metaclass(abc.ABCMeta) +class FarmRootEnumEntity(BaseEnumEntity): + schema_types = [] def _item_initialization(self): self.multiselection = self.schema_data.get("multiselection", True) @@ -495,22 +498,8 @@ class DeadlineUrlEnumEntity(BaseEnumEntity): # GUI attribute self.placeholder = self.schema_data.get("placeholder") - def _get_enum_values(self): - deadline_urls_entity = self.get_entity_from_path( - "system_settings/modules/deadline/deadline_urls" - ) - - valid_keys = set() - enum_items_list = [] - for server_name, url_entity in deadline_urls_entity.items(): - enum_items_list.append( - {server_name: "{}: {}".format(server_name, url_entity.value)} - ) - valid_keys.add(server_name) - return enum_items_list, valid_keys - def set_override_state(self, *args, **kwargs): - super(DeadlineUrlEnumEntity, self).set_override_state(*args, **kwargs) + super(FarmRootEnumEntity, self).set_override_state(*args, **kwargs) self.enum_items, self.valid_keys = self._get_enum_values() if self.multiselection: @@ -527,22 +516,50 @@ class DeadlineUrlEnumEntity(BaseEnumEntity): elif self._current_value not in self.valid_keys: self._current_value = tuple(self.valid_keys)[0] + @abc.abstractmethod + def _get_enum_values(self): + pass -class ShotgridUrlEnumEntity(BaseEnumEntity): + +class DeadlineUrlEnumEntity(FarmRootEnumEntity): + schema_types = ["deadline_url-enum"] + + def _get_enum_values(self): + deadline_urls_entity = self.get_entity_from_path( + "system_settings/modules/deadline/deadline_urls" + ) + + valid_keys = set() + enum_items_list = [] + for server_name, url_entity in deadline_urls_entity.items(): + enum_items_list.append( + {server_name: "{}: {}".format(server_name, url_entity.value)} + ) + valid_keys.add(server_name) + return enum_items_list, valid_keys + + +class RoyalRenderRootEnumEntity(FarmRootEnumEntity): + schema_types = ["rr_root-enum"] + + def _get_enum_values(self): + rr_root_entity = self.get_entity_from_path( + "system_settings/modules/royalrender/rr_paths" + ) + + valid_keys = set() + enum_items_list = [] + for server_name, url_entity in rr_root_entity.items(): + enum_items_list.append( + {server_name: "{}: {}".format(server_name, url_entity.value)} + ) + valid_keys.add(server_name) + return enum_items_list, valid_keys + + +class ShotgridUrlEnumEntity(FarmRootEnumEntity): schema_types = ["shotgrid_url-enum"] - def _item_initialization(self): - self.multiselection = False - - self.enum_items = [] - self.valid_keys = set() - - self.valid_value_types = (STRING_TYPE,) - self.value_on_not_set = "" - - # GUI attribute - self.placeholder = self.schema_data.get("placeholder") - def _get_enum_values(self): shotgrid_settings = self.get_entity_from_path( "system_settings/modules/shotgrid/shotgrid_settings" @@ -561,16 +578,6 @@ class ShotgridUrlEnumEntity(BaseEnumEntity): valid_keys.add(server_name) return enum_items_list, valid_keys - def set_override_state(self, *args, **kwargs): - super(ShotgridUrlEnumEntity, self).set_override_state(*args, **kwargs) - - self.enum_items, self.valid_keys = self._get_enum_values() - if not self.valid_keys: - self._current_value = "" - - elif self._current_value not in self.valid_keys: - self._current_value = tuple(self.valid_keys)[0] - class AnatomyTemplatesEnumEntity(BaseEnumEntity): schema_types = ["anatomy-templates-enum"] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_royalrender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_royalrender.json index cabb4747d5..f4bf2f51ba 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_royalrender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_royalrender.json @@ -5,6 +5,12 @@ "collapsible": true, "is_file": true, "children": [ + { + "type": "rr_root-enum", + "key": "rr_paths", + "label": "Royal Render Roots", + "multiselect": true + }, { "type": "dict", "collapsible": true, From 2cc688fb72dd9d04bbe76beab2ca3d04c4c9df1d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 16 Jan 2023 10:40:05 +0100 Subject: [PATCH 005/446] :construction: render job submission --- openpype/modules/royalrender/api.py | 76 +++++++++++-------- .../publish/submit_maya_royalrender.py | 45 ++++++----- openpype/modules/royalrender/rr_job.py | 10 ++- 3 files changed, 77 insertions(+), 54 deletions(-) diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index dcb518deb1..8b13b9781f 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -2,11 +2,13 @@ """Wrapper around Royal Render API.""" import sys import os +import platform from openpype.settings import get_project_settings from openpype.lib.local_settings import OpenPypeSettingsRegistry from openpype.lib import Logger, run_subprocess from .rr_job import RRJob, SubmitFile, SubmitterParameter +from openpype.lib.vendor_bin_utils import find_tool_in_custom_paths class Api: @@ -20,31 +22,46 @@ class Api: self._rr_path = rr_path os.environ["RR_ROOT"] = rr_path - def _get_rr_bin_path(self, rr_root=None): - # type: (str) -> str - """Get path to RR bin folder.""" + def _get_rr_bin_path(self, tool_name=None, rr_root=None): + # type: (str, str) -> str + """Get path to RR bin folder. + + Args: + tool_name (str): Name of RR executable you want. + rr_root (str, Optional): Custom RR root if needed. + + Returns: + str: Path to the tool based on current platform. + + """ rr_root = rr_root or self._rr_path is_64bit_python = sys.maxsize > 2 ** 32 - rr_bin_path = "" + rr_bin_parts = [rr_root, "bin"] if sys.platform.lower() == "win32": - rr_bin_path = "/bin/win64" - if not is_64bit_python: - # we are using 64bit python - rr_bin_path = "/bin/win" - rr_bin_path = rr_bin_path.replace( - "/", os.path.sep - ) + rr_bin_parts.append("win") if sys.platform.lower() == "darwin": - rr_bin_path = "/bin/mac64" - if not is_64bit_python: - rr_bin_path = "/bin/mac" + rr_bin_parts.append("mac") - if sys.platform.lower() == "linux": - rr_bin_path = "/bin/lx64" + if sys.platform.lower().startswith("linux"): + rr_bin_parts.append("lx") - return os.path.join(rr_root, rr_bin_path) + rr_bin_path = os.sep.join(rr_bin_parts) + + paths_to_check = [] + # if we use 64bit python, append 64bit specific path first + if is_64bit_python: + if not tool_name: + return rr_bin_path + "64" + paths_to_check.append(rr_bin_path + "64") + + # otherwise use 32bit + if not tool_name: + return rr_bin_path + paths_to_check.append(rr_bin_path) + + return find_tool_in_custom_paths(paths_to_check, tool_name) def _initialize_module_path(self): # type: () -> None @@ -84,30 +101,25 @@ class Api: # type: (SubmitFile, int) -> None if mode == self.RR_SUBMIT_CONSOLE: self._submit_using_console(file) + return - # RR v7 supports only Python 2.7 so we bail out in fear + # RR v7 supports only Python 2.7, so we bail out in fear # until there is support for Python 3 😰 raise NotImplementedError( "Submission via RoyalRender API is not supported yet") # self._submit_using_api(file) - def _submit_using_console(self, file): + def _submit_using_console(self, job_file): # type: (SubmitFile) -> None - rr_console = os.path.join( - self._get_rr_bin_path(), - "rrSubmitterConsole" - ) + rr_start_local = self._get_rr_bin_path("rrStartLocal") - if sys.platform.lower() == "darwin" and "/bin/mac64" in rr_console: - rr_console = rr_console.replace("/bin/mac64", "/bin/mac") + self.log.info("rr_console: {}".format(rr_start_local)) - if sys.platform.lower() == "win32": - if "/bin/win64" in rr_console: - rr_console = rr_console.replace("/bin/win64", "/bin/win") - rr_console += ".exe" - - args = [rr_console, file] - run_subprocess(" ".join(args), logger=self.log) + args = [rr_start_local, "rrSubmitterconsole", job_file] + self.log.info("Executing: {}".format(" ".join(args))) + env = os.environ + env["RR_ROOT"] = self._rr_path + run_subprocess(args, logger=self.log, env=env) def _submit_using_api(self, file): # type: (SubmitFile) -> None diff --git a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py index ef98bcf74c..9303bad895 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py @@ -38,11 +38,11 @@ class MayaSubmitRoyalRender(InstancePlugin): """ def get_rr_platform(): if sys.platform.lower() in ["win32", "win64"]: - return "win" + return "windows" elif sys.platform.lower() == "darwin": return "mac" else: - return "lx" + return "linux" expected_files = self._instance.data["expectedFiles"] first_file = next(self._iter_expected_files(expected_files)) @@ -53,8 +53,10 @@ class MayaSubmitRoyalRender(InstancePlugin): .get('maya') \ .get('RenderSettings') \ .get('default_render_image_folder') - filename = os.path.basename(self.scene_path) - dirname = os.path.join(workspace, default_render_file) + file_name = os.path.basename(self.scene_path) + dir_name = os.path.join(workspace, default_render_file) + layer = self._instance.data["setMembers"] # type: str + layer_name = layer.removeprefix("rs_") job = RRJob( Software="Maya", @@ -64,22 +66,22 @@ class MayaSubmitRoyalRender(InstancePlugin): SeqStep=int(self._instance.data["byFrameStep"]), SeqFileOffset=0, Version="{0:.2f}".format(MGlobal.apiVersion() / 10000), - SceneName=os.path.basename(self.scene_path), + SceneName=self.scene_path, IsActive=True, - ImageDir=dirname, - ImageFilename=filename, - ImageExtension="." + os.path.splitext(filename)[1], + ImageDir=dir_name, + ImageFilename="{}.".format(layer_name), + ImageExtension=os.path.splitext(first_file)[1], ImagePreNumberLetter=".", ImageSingleOutputFile=False, SceneOS=get_rr_platform(), Camera=self._instance.data["cameras"][0], - Layer=self._instance.data["layer"], + Layer=layer_name, SceneDatabaseDir=workspace, - ImageFramePadding=get_attr_in_layer( - "defaultRenderGlobals.extensionPadding", - self._instance.data["layer"]), + CustomSHotName=self._instance.context.data["asset"], + CompanyProjectName=self._instance.context.data["projectName"], ImageWidth=self._instance.data["resolutionWidth"], - ImageHeight=self._instance.data["resolutionHeight"] + ImageHeight=self._instance.data["resolutionHeight"], + PreID=1 ) return job @@ -125,11 +127,9 @@ class MayaSubmitRoyalRender(InstancePlugin): self.log.error("Cannot get OpenPype RoyalRender module.") raise AssertionError("OpenPype RoyalRender module not found.") """ - - file_path = None if self.use_published: - file_path = get_published_workfile_instance() + file_path = get_published_workfile_instance(context) # fallback if nothing was set if not file_path: @@ -153,7 +153,8 @@ class MayaSubmitRoyalRender(InstancePlugin): with open(xml.name, "w") as f: f.write(submission.serialize()) - self.rr_api.submit_file(file=xml) + self.log.info("submitting job file: {}".format(xml.name)) + self.rr_api.submit_file(file=xml.name) @staticmethod def _resolve_rr_path(context, rr_path_name): @@ -184,4 +185,12 @@ class MayaSubmitRoyalRender(InstancePlugin): return rr_servers[rr_path_name][platform.system().lower()] - + @staticmethod + def _iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index beb8c17187..f5c7033b62 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -87,7 +87,7 @@ class RRJob: # Frame Padding of the frame number in the rendered filename. # Some render config files are setting the padding at render time. - ImageFramePadding = attr.ib(default=None) # type: str + ImageFramePadding = attr.ib(default=None) # type: int # Some render applications support overriding the image format at # the render commandline. @@ -129,6 +129,7 @@ class RRJob: CustomUserInfo = attr.ib(default=None) # type: str SubmitMachine = attr.ib(default=None) # type: str Color_ID = attr.ib(default=2) # type: int + CompanyProjectName = attr.ib(default=None) # type: str RequiredLicenses = attr.ib(default=None) # type: str @@ -225,7 +226,7 @@ class SubmitFile: # foo=bar~baz~goo self._process_submitter_parameters( self.SubmitterParameters, root, job_file) - + root.appendChild(job_file) for job in self.Jobs: # type: RRJob if not isinstance(job, RRJob): raise AttributeError( @@ -247,10 +248,11 @@ class SubmitFile: custom_attr.name)] = custom_attr.value for item, value in serialized_job.items(): - xml_attr = root.create(item) + xml_attr = root.createElement(item) xml_attr.appendChild( - root.createTextNode(value) + root.createTextNode(str(value)) ) xml_job.appendChild(xml_attr) + job_file.appendChild(xml_job) return root.toprettyxml(indent="\t") From 1e386c7ce4a30def013780ca05896de8365a2d7a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 16 Jan 2023 13:14:41 +0000 Subject: [PATCH 006/446] Implemented Blender Scene file type --- .../plugins/create/create_blender_scene.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 openpype/hosts/blender/plugins/create/create_blender_scene.py diff --git a/openpype/hosts/blender/plugins/create/create_blender_scene.py b/openpype/hosts/blender/plugins/create/create_blender_scene.py new file mode 100644 index 0000000000..b63ed4fd3f --- /dev/null +++ b/openpype/hosts/blender/plugins/create/create_blender_scene.py @@ -0,0 +1,37 @@ +import bpy + +from openpype.pipeline import legacy_io +from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES + + +class CreateBlenderScene(plugin.Creator): + """Raw Blender Scene file export""" + + name = "blenderScene" + label = "Blender Scene" + family = "blenderScene" + icon = "file-archive-o" + + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process) + ops.execute_in_main_thread(mti) + + def _process(self): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + asset = self.data["asset"] + subset = self.data["subset"] + name = plugin.asset_name(asset, subset) + + asset_group = bpy.data.collections.new(name=name) + instances.children.link(asset_group) + self.data['task'] = legacy_io.Session.get('AVALON_TASK') + lib.imprint(asset_group, self.data) + + return asset_group From 44b45cdee00cd0769466b5195fa72aef8335a324 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 16 Jan 2023 13:15:35 +0000 Subject: [PATCH 007/446] Implemented Blender Scene extraction --- .../publish/extract_blender_scene_raw.py | 93 +++++++++++++++++++ openpype/plugins/publish/integrate_legacy.py | 3 +- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py diff --git a/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py b/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py new file mode 100644 index 0000000000..d6c858aaed --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py @@ -0,0 +1,93 @@ +import os + +import bpy + +from openpype.pipeline import AVALON_CONTAINER_ID, publish +from openpype.hosts.blender.api.workio import save_file + + +class ExtractBlenderSceneRaw(publish.Extractor): + """Extract as Blender Scene (raw). + + This will preserve all references, construction history, etc. + """ + + label = "Blender Scene (Raw)" + hosts = ["blender"] + families = ["blenderScene", "layout"] + scene_type = "blend" + + def process(self, instance): + # Define extract output file path + dir_path = self.staging_dir(instance) + filename = "{0}.{1}".format(instance.name, self.scene_type) + path = os.path.join(dir_path, filename) + + # We need to get all the data blocks for all the blender objects. + # The following set will contain all the data blocks from version + # 2.93 of Blender. + data_blocks = { + *bpy.data.actions, + *bpy.data.armatures, + *bpy.data.brushes, + *bpy.data.cache_files, + *bpy.data.cameras, + *bpy.data.collections, + *bpy.data.curves, + *bpy.data.fonts, + *bpy.data.grease_pencils, + *bpy.data.images, + *bpy.data.lattices, + *bpy.data.libraries, + *bpy.data.lightprobes, + *bpy.data.lights, + *bpy.data.linestyles, + *bpy.data.masks, + *bpy.data.materials, + *bpy.data.metaballs, + *bpy.data.meshes, + *bpy.data.movieclips, + *bpy.data.node_groups, + *bpy.data.objects, + *bpy.data.paint_curves, + *bpy.data.palettes, + *bpy.data.particles, + *bpy.data.scenes, + *bpy.data.screens, + *bpy.data.shape_keys, + *bpy.data.speakers, + *bpy.data.sounds, + *bpy.data.texts, + *bpy.data.textures, + *bpy.data.volumes, + *bpy.data.worlds + } + + # Some data blocks are only available in certain versions of Blender. + version = tuple(map(int, (bpy.app.version_string.split(".")))) + + if version >= (3, 0, 0): + data_blocks |= { + *bpy.data.pointclouds + } + + if version >= (3, 3, 0): + data_blocks |= { + *bpy.data.hair_curves + } + + # Write the datablocks in a new blend file. + bpy.data.libraries.write(path, data_blocks) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': self.scene_type, + 'ext': self.scene_type, + 'files': filename, + "stagingDir": dir_path + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index b93abab1d8..c2abd66446 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -125,7 +125,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "mvLook", "mvUsdComposition", "mvUsdOverride", - "simpleUnrealTexture" + "simpleUnrealTexture", + "blenderScene" ] exclude_families = ["render.farm"] db_representation_context_keys = [ From 7f31e88acf9383abce4594a736089a4ecd9a5f79 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 16 Jan 2023 15:30:10 +0000 Subject: [PATCH 008/446] Improved selection of data blocks to extract --- .../publish/extract_blender_scene_raw.py | 56 ++----------------- 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py b/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py index d6c858aaed..b3f6f6460c 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py +++ b/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py @@ -2,8 +2,7 @@ import os import bpy -from openpype.pipeline import AVALON_CONTAINER_ID, publish -from openpype.hosts.blender.api.workio import save_file +from openpype.pipeline import publish class ExtractBlenderSceneRaw(publish.Extractor): @@ -26,55 +25,12 @@ class ExtractBlenderSceneRaw(publish.Extractor): # We need to get all the data blocks for all the blender objects. # The following set will contain all the data blocks from version # 2.93 of Blender. - data_blocks = { - *bpy.data.actions, - *bpy.data.armatures, - *bpy.data.brushes, - *bpy.data.cache_files, - *bpy.data.cameras, - *bpy.data.collections, - *bpy.data.curves, - *bpy.data.fonts, - *bpy.data.grease_pencils, - *bpy.data.images, - *bpy.data.lattices, - *bpy.data.libraries, - *bpy.data.lightprobes, - *bpy.data.lights, - *bpy.data.linestyles, - *bpy.data.masks, - *bpy.data.materials, - *bpy.data.metaballs, - *bpy.data.meshes, - *bpy.data.movieclips, - *bpy.data.node_groups, - *bpy.data.objects, - *bpy.data.paint_curves, - *bpy.data.palettes, - *bpy.data.particles, - *bpy.data.scenes, - *bpy.data.screens, - *bpy.data.shape_keys, - *bpy.data.speakers, - *bpy.data.sounds, - *bpy.data.texts, - *bpy.data.textures, - *bpy.data.volumes, - *bpy.data.worlds - } + data_blocks = set() - # Some data blocks are only available in certain versions of Blender. - version = tuple(map(int, (bpy.app.version_string.split(".")))) - - if version >= (3, 0, 0): - data_blocks |= { - *bpy.data.pointclouds - } - - if version >= (3, 3, 0): - data_blocks |= { - *bpy.data.hair_curves - } + for attr in dir(bpy.data): + data_block = getattr(bpy.data, attr) + if isinstance(data_block, bpy.types.bpy_prop_collection): + data_blocks |= {*data_block} # Write the datablocks in a new blend file. bpy.data.libraries.write(path, data_blocks) From b3b266752bc2396fa7d68637f25e5725283e54d6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jan 2023 17:06:05 +0100 Subject: [PATCH 009/446] :construction: refactor RR job flow --- .../publish/collect_rr_path_from_instance.py | 12 +- ...nder.py => create_maya_royalrender_job.py} | 76 ++------ .../publish/create_publish_royalrender_job.py | 178 ++++++++++++++++++ .../publish/submit_jobs_to_royalrender.py | 115 +++++++++++ openpype/pipeline/farm/pyblish.py | 49 +++++ 5 files changed, 364 insertions(+), 66 deletions(-) rename openpype/modules/royalrender/plugins/publish/{submit_maya_royalrender.py => create_maya_royalrender_job.py} (74%) create mode 100644 openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py create mode 100644 openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py create mode 100644 openpype/pipeline/farm/pyblish.py diff --git a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index 187e2b9c44..40f34561fa 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -15,19 +15,21 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): "Using '{}' for submission.".format(instance.data["rrPathName"])) @staticmethod - def _collect_rr_path_name(render_instance): + def _collect_rr_path_name(instance): # type: (pyblish.api.Instance) -> str """Get Royal Render pat name from render instance.""" rr_settings = ( - render_instance.context.data + instance.context.data ["system_settings"] ["modules"] ["royalrender"] ) + if not instance.data.get("rrPaths"): + return "default" try: default_servers = rr_settings["rr_paths"] project_servers = ( - render_instance.context.data + instance.context.data ["project_settings"] ["royalrender"] ["rr_paths"] @@ -40,8 +42,8 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): except (AttributeError, KeyError): # Handle situation were we had only one url for royal render. - return render_instance.context.data["defaultRRPath"] + return rr_settings["rr_paths"]["default"] return list(rr_servers.keys())[ - int(render_instance.data.get("rrPaths")) + int(instance.data.get("rrPaths")) ] diff --git a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py similarity index 74% rename from openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py rename to openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 9303bad895..e194a7edc6 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -2,20 +2,18 @@ """Submitting render job to RoyalRender.""" import os import sys -import tempfile import platform from maya.OpenMaya import MGlobal # noqa from pyblish.api import InstancePlugin, IntegratorOrder, Context -from openpype.hosts.maya.api.lib import get_attr_in_layer from openpype.pipeline.publish.lib import get_published_workfile_instance from openpype.pipeline.publish import KnownPublishError -from openpype.modules.royalrender.api import Api as rr_api -from openpype.modules.royalrender.rr_job import RRJob, SubmitterParameter +from openpype.modules.royalrender.api import Api as rrApi +from openpype.modules.royalrender.rr_job import RRJob -class MayaSubmitRoyalRender(InstancePlugin): - label = "Submit to RoyalRender" +class CreateMayaRoyalRenderJob(InstancePlugin): + label = "Create Maya Render job in RR" order = IntegratorOrder + 0.1 families = ["renderlayer"] targets = ["local"] @@ -81,28 +79,9 @@ class MayaSubmitRoyalRender(InstancePlugin): CompanyProjectName=self._instance.context.data["projectName"], ImageWidth=self._instance.data["resolutionWidth"], ImageHeight=self._instance.data["resolutionHeight"], - PreID=1 ) return job - @staticmethod - def get_submission_parameters(): - return [] - - def create_file(self, name, ext, contents=None): - temp = tempfile.NamedTemporaryFile( - dir=self.tempdir, - suffix=ext, - prefix=name + '.', - delete=False, - ) - - if contents: - with open(temp.name, 'w') as f: - f.write(contents) - - return temp.name - def process(self, instance): """Plugin entry point.""" self._instance = instance @@ -116,17 +95,8 @@ class MayaSubmitRoyalRender(InstancePlugin): ("Missing RoyalRender root. " "You need to configure RoyalRender module.")) - self.rr_api = rr_api(self._rr_root) + self.rr_api = rrApi(self._rr_root) - # get royalrender module - """ - try: - rr_module = context.data.get( - "openPypeModules")["royalrender"] - except AttributeError: - self.log.error("Cannot get OpenPype RoyalRender module.") - raise AssertionError("OpenPype RoyalRender module not found.") - """ file_path = None if self.use_published: file_path = get_published_workfile_instance(context) @@ -137,24 +107,18 @@ class MayaSubmitRoyalRender(InstancePlugin): file_path = context.data["currentFile"] self.scene_path = file_path - self.job = self.get_job() - self.log.info(self.job) - self.submission_parameters = self.get_submission_parameters() - self.process_submission() + self._instance.data["rrJobs"] = [self.get_job()] - def process_submission(self): - submission = rr_api.create_submission( - [self.job], - self.submission_parameters) - - self.log.debug(submission) - xml = tempfile.NamedTemporaryFile(suffix=".xml", delete=False) - with open(xml.name, "w") as f: - f.write(submission.serialize()) - - self.log.info("submitting job file: {}".format(xml.name)) - self.rr_api.submit_file(file=xml.name) + @staticmethod + def _iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file @staticmethod def _resolve_rr_path(context, rr_path_name): @@ -184,13 +148,3 @@ class MayaSubmitRoyalRender(InstancePlugin): return context.data["defaultRRPath"][platform.system().lower()] return rr_servers[rr_path_name][platform.system().lower()] - - @staticmethod - def _iter_expected_files(exp): - if isinstance(exp[0], dict): - for _aov, files in exp[0].items(): - for file in files: - yield file - else: - for file in exp: - yield file diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py new file mode 100644 index 0000000000..9d8cc602c5 --- /dev/null +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +"""Create publishing job on RoyalRender.""" +from pyblish.api import InstancePlugin, IntegratorOrder +from copy import deepcopy +from openpype.pipeline import legacy_io +import requests +import os + + +class CreatePublishRoyalRenderJob(InstancePlugin): + label = "Create publish job in RR" + order = IntegratorOrder + 0.2 + icon = "tractor" + targets = ["local"] + hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"] + families = ["render.farm", "prerender.farm", + "renderlayer", "imagesequence", "vrayscene"] + aov_filter = {"maya": [r".*([Bb]eauty).*"], + "aftereffects": [r".*"], # for everything from AE + "harmony": [r".*"], # for everything from AE + "celaction": [r".*"]} + + def process(self, instance): + data = instance.data.copy() + context = instance.context + self.context = context + self.anatomy = instance.context.data["anatomy"] + + asset = data.get("asset") + subset = data.get("subset") + source = self._remap_source( + data.get("source") or context.data["source"]) + + + + def _remap_source(self, source): + success, rootless_path = ( + self.anatomy.find_root_template_from_path(source) + ) + if success: + source = rootless_path + else: + # `rootless_path` is not set to `source` if none of roots match + self.log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues." + ).format(source)) + return source + + def _submit_post_job(self, instance, job, instances): + """Submit publish job to RoyalRender.""" + data = instance.data.copy() + subset = data["subset"] + job_name = "Publish - {subset}".format(subset=subset) + + # instance.data.get("subset") != instances[0]["subset"] + # 'Main' vs 'renderMain' + override_version = None + instance_version = instance.data.get("version") # take this if exists + if instance_version != 1: + override_version = instance_version + output_dir = self._get_publish_folder( + instance.context.data['anatomy'], + deepcopy(instance.data["anatomyData"]), + instance.data.get("asset"), + instances[0]["subset"], + 'render', + override_version + ) + + # Transfer the environment from the original job to this dependent + # job, so they use the same environment + metadata_path, roothless_metadata_path = \ + self._create_metadata_path(instance) + + environment = { + "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], + "AVALON_ASSET": legacy_io.Session["AVALON_ASSET"], + "AVALON_TASK": legacy_io.Session["AVALON_TASK"], + "OPENPYPE_USERNAME": instance.context.data["user"], + "OPENPYPE_PUBLISH_JOB": "1", + "OPENPYPE_RENDER_JOB": "0", + "OPENPYPE_REMOTE_JOB": "0", + "OPENPYPE_LOG_NO_COLORS": "1" + } + + # add environments from self.environ_keys + for env_key in self.environ_keys: + if os.getenv(env_key): + environment[env_key] = os.environ[env_key] + + # pass environment keys from self.environ_job_filter + job_environ = job["Props"].get("Env", {}) + for env_j_key in self.environ_job_filter: + if job_environ.get(env_j_key): + environment[env_j_key] = job_environ[env_j_key] + + # Add mongo url if it's enabled + if instance.context.data.get("deadlinePassMongoUrl"): + mongo_url = os.environ.get("OPENPYPE_MONGO") + if mongo_url: + environment["OPENPYPE_MONGO"] = mongo_url + + priority = self.deadline_priority or instance.data.get("priority", 50) + + args = [ + "--headless", + 'publish', + roothless_metadata_path, + "--targets", "deadline", + "--targets", "farm" + ] + + # Generate the payload for Deadline submission + payload = { + "JobInfo": { + "Plugin": self.deadline_plugin, + "BatchName": job["Props"]["Batch"], + "Name": job_name, + "UserName": job["Props"]["User"], + "Comment": instance.context.data.get("comment", ""), + + "Department": self.deadline_department, + "ChunkSize": self.deadline_chunk_size, + "Priority": priority, + + "Group": self.deadline_group, + "Pool": instance.data.get("primaryPool"), + "SecondaryPool": instance.data.get("secondaryPool"), + + "OutputDirectory0": output_dir + }, + "PluginInfo": { + "Version": self.plugin_pype_version, + "Arguments": " ".join(args), + "SingleFrameOnly": "True", + }, + # Mandatory for Deadline, may be empty + "AuxFiles": [], + } + + # add assembly jobs as dependencies + if instance.data.get("tileRendering"): + self.log.info("Adding tile assembly jobs as dependencies...") + job_index = 0 + for assembly_id in instance.data.get("assemblySubmissionJobs"): + payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 + job_index += 1 + elif instance.data.get("bakingSubmissionJobs"): + self.log.info("Adding baking submission jobs as dependencies...") + job_index = 0 + for assembly_id in instance.data["bakingSubmissionJobs"]: + payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 + job_index += 1 + else: + payload["JobInfo"]["JobDependency0"] = job["_id"] + + if instance.data.get("suspend_publish"): + payload["JobInfo"]["InitialStatus"] = "Suspended" + + for index, (key_, value_) in enumerate(environment.items()): + payload["JobInfo"].update( + { + "EnvironmentKeyValue%d" + % index: "{key}={value}".format( + key=key_, value=value_ + ) + } + ) + # remove secondary pool + payload["JobInfo"].pop("SecondaryPool", None) + + self.log.info("Submitting Deadline job ...") + + url = "{}/api/jobs".format(self.deadline_url) + response = requests.post(url, json=payload, timeout=10) + if not response.ok: + raise Exception(response.text) \ No newline at end of file diff --git a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py new file mode 100644 index 0000000000..4fcf3a08bd --- /dev/null +++ b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +"""Submit jobs to RoyalRender.""" +import tempfile +import platform + +from pyblish.api import IntegratorOrder, ContextPlugin, Context +from openpype.modules.royalrender.api import RRJob, Api as rrApi +from openpype.pipeline.publish import KnownPublishError + + +class SubmitJobsToRoyalRender(ContextPlugin): + """Find all jobs, create submission XML and submit it to RoyalRender.""" + label = "Submit jobs to RoyalRender" + order = IntegratorOrder + 0.1 + targets = ["local"] + + def __init__(self): + super(SubmitJobsToRoyalRender, self).__init__() + self._rr_root = None + self._rr_api = None + self._submission_parameters = [] + + def process(self, context): + rr_settings = ( + context.data + ["system_settings"] + ["modules"] + ["royalrender"] + ) + + if rr_settings["enabled"] is not True: + self.log.warning("RoyalRender modules is disabled.") + return + + # iterate over all instances and try to find RRJobs + jobs = [] + for instance in context: + if isinstance(instance.data.get("rrJob"), RRJob): + jobs.append(instance.data.get("rrJob")) + if instance.data.get("rrJobs"): + if all(isinstance(job, RRJob) for job in instance.data.get("rrJobs")): + jobs += instance.data.get("rrJobs") + + if jobs: + self._rr_root = self._resolve_rr_path( + context, instance.data.get("rrPathName")) # noqa + if not self._rr_root: + raise KnownPublishError( + ("Missing RoyalRender root. " + "You need to configure RoyalRender module.")) + self._rr_api = rrApi(self._rr_root) + self._submission_parameters = self.get_submission_parameters() + self.process_submission(jobs) + return + + self.log.info("No RoyalRender jobs found") + + def process_submission(self, jobs): + # type: ([RRJob]) -> None + submission = rrApi.create_submission( + jobs, + self._submission_parameters) + + xml = tempfile.NamedTemporaryFile(suffix=".xml", delete=False) + with open(xml.name, "w") as f: + f.write(submission.serialize()) + + self.log.info("submitting job(s) file: {}".format(xml.name)) + self._rr_api.submit_file(file=xml.name) + + def create_file(self, name, ext, contents=None): + temp = tempfile.NamedTemporaryFile( + dir=self.tempdir, + suffix=ext, + prefix=name + '.', + delete=False, + ) + + if contents: + with open(temp.name, 'w') as f: + f.write(contents) + + return temp.name + + def get_submission_parameters(self): + return [] + + @staticmethod + def _resolve_rr_path(context, rr_path_name): + # type: (Context, str) -> str + rr_settings = ( + context.data + ["system_settings"] + ["modules"] + ["royalrender"] + ) + try: + default_servers = rr_settings["rr_paths"] + project_servers = ( + context.data + ["project_settings"] + ["royalrender"] + ["rr_paths"] + ) + rr_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + + except (AttributeError, KeyError): + # Handle situation were we had only one url for royal render. + return context.data["defaultRRPath"][platform.system().lower()] + + return rr_servers[rr_path_name][platform.system().lower()] diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish.py new file mode 100644 index 0000000000..02535f4090 --- /dev/null +++ b/openpype/pipeline/farm/pyblish.py @@ -0,0 +1,49 @@ +from openpype.lib import Logger +import attr + + +@attr.s +class InstanceSkeleton(object): + family = attr.ib(factory=) + +def remap_source(source, anatomy): + success, rootless_path = ( + anatomy.find_root_template_from_path(source) + ) + if success: + source = rootless_path + else: + # `rootless_path` is not set to `source` if none of roots match + log = Logger.get_logger("farm_publishing") + log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues." + ).format(source)) + return source + +def get_skeleton_instance() + instance_skeleton_data = { + "family": family, + "subset": subset, + "families": families, + "asset": asset, + "frameStart": start, + "frameEnd": end, + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStartHandle": start - handle_start, + "frameEndHandle": end + handle_end, + "comment": instance.data["comment"], + "fps": fps, + "source": source, + "extendFrames": data.get("extendFrames"), + "overrideExistingFrame": data.get("overrideExistingFrame"), + "pixelAspect": data.get("pixelAspect", 1), + "resolutionWidth": data.get("resolutionWidth", 1920), + "resolutionHeight": data.get("resolutionHeight", 1080), + "multipartExr": data.get("multipartExr", False), + "jobBatchName": data.get("jobBatchName", ""), + "useSequenceForReview": data.get("useSequenceForReview", True), + # map inputVersions `ObjectId` -> `str` so json supports it + "inputVersions": list(map(str, data.get("inputVersions", []))) + } \ No newline at end of file From 60eaf283ed0621aab5c289e5bd6537a80a9af0f2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jan 2023 17:06:53 +0100 Subject: [PATCH 010/446] :bug: fix default for ShotGrid this is fixing default value for ShotGrid server enumerator after code refactor done in this branch --- openpype/settings/defaults/project_settings/shotgrid.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/shotgrid.json b/openpype/settings/defaults/project_settings/shotgrid.json index 774bce714b..143a474b80 100644 --- a/openpype/settings/defaults/project_settings/shotgrid.json +++ b/openpype/settings/defaults/project_settings/shotgrid.json @@ -1,6 +1,6 @@ { "shotgrid_project_id": 0, - "shotgrid_server": "", + "shotgrid_server": [], "event": { "enabled": false }, From b4f1574d443f0abc963a946dcaa287faf8c3deac Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jan 2023 17:19:18 +0100 Subject: [PATCH 011/446] :rotating_light: hound fixes --- .../hosts/maya/plugins/create/create_render.py | 2 +- openpype/modules/royalrender/api.py | 2 -- .../publish/collect_rr_path_from_instance.py | 4 +--- .../plugins/publish/create_maya_royalrender_job.py | 14 ++++++++------ .../publish/create_publish_royalrender_job.py | 12 +++++------- .../plugins/publish/submit_jobs_to_royalrender.py | 4 +++- openpype/pipeline/farm/pyblish.py | 12 +++++++++--- 7 files changed, 27 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index dedb057fb7..4189351238 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -278,7 +278,7 @@ class CreateRender(plugin.Creator): if deadline_enabled and muster_enabled and royalrender_enabled: self.log.error( - ("Multiple render farm support (Deadline/RoyalRender/Muster) " + ("Multiple render farm support (Deadline/RoyalRender/Muster) " "is enabled. We support only one at time.") ) raise RuntimeError("Both Deadline and Muster are enabled") diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index 8b13b9781f..86d27ccc6c 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -2,9 +2,7 @@ """Wrapper around Royal Render API.""" import sys import os -import platform -from openpype.settings import get_project_settings from openpype.lib.local_settings import OpenPypeSettingsRegistry from openpype.lib import Logger, run_subprocess from .rr_job import RRJob, SubmitFile, SubmitterParameter diff --git a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index 40f34561fa..cfb5b78077 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -44,6 +44,4 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): # Handle situation were we had only one url for royal render. return rr_settings["rr_paths"]["default"] - return list(rr_servers.keys())[ - int(instance.data.get("rrPaths")) - ] + return list(rr_servers.keys())[int(instance.data.get("rrPaths"))] diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index e194a7edc6..5f427353ac 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -47,11 +47,14 @@ class CreateMayaRoyalRenderJob(InstancePlugin): output_dir = os.path.dirname(first_file) self._instance.data["outputDir"] = output_dir workspace = self._instance.context.data["workspaceDir"] - default_render_file = self._instance.context.data.get('project_settings') \ - .get('maya') \ - .get('RenderSettings') \ - .get('default_render_image_folder') - file_name = os.path.basename(self.scene_path) + default_render_file = ( + self._instance.context.data + ['project_settings'] + ['maya'] + ['RenderSettings'] + ['default_render_image_folder'] + ) + # file_name = os.path.basename(self.scene_path) dir_name = os.path.join(workspace, default_render_file) layer = self._instance.data["setMembers"] # type: str layer_name = layer.removeprefix("rs_") @@ -86,7 +89,6 @@ class CreateMayaRoyalRenderJob(InstancePlugin): """Plugin entry point.""" self._instance = instance context = instance.context - from pprint import pformat self._rr_root = self._resolve_rr_path(context, instance.data.get("rrPathName")) # noqa self.log.debug(self._rr_root) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 9d8cc602c5..e62289641b 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -26,12 +26,10 @@ class CreatePublishRoyalRenderJob(InstancePlugin): self.context = context self.anatomy = instance.context.data["anatomy"] - asset = data.get("asset") - subset = data.get("subset") - source = self._remap_source( - data.get("source") or context.data["source"]) - - + # asset = data.get("asset") + # subset = data.get("subset") + # source = self._remap_source( + # data.get("source") or context.data["source"]) def _remap_source(self, source): success, rootless_path = ( @@ -175,4 +173,4 @@ class CreatePublishRoyalRenderJob(InstancePlugin): url = "{}/api/jobs".format(self.deadline_url) response = requests.post(url, json=payload, timeout=10) if not response.ok: - raise Exception(response.text) \ No newline at end of file + raise Exception(response.text) diff --git a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py index 4fcf3a08bd..325fb36993 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -38,7 +38,9 @@ class SubmitJobsToRoyalRender(ContextPlugin): if isinstance(instance.data.get("rrJob"), RRJob): jobs.append(instance.data.get("rrJob")) if instance.data.get("rrJobs"): - if all(isinstance(job, RRJob) for job in instance.data.get("rrJobs")): + if all( + isinstance(job, RRJob) + for job in instance.data.get("rrJobs")): jobs += instance.data.get("rrJobs") if jobs: diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish.py index 02535f4090..15f4356b86 100644 --- a/openpype/pipeline/farm/pyblish.py +++ b/openpype/pipeline/farm/pyblish.py @@ -4,7 +4,9 @@ import attr @attr.s class InstanceSkeleton(object): - family = attr.ib(factory=) + # family = attr.ib(factory=) + pass + def remap_source(source, anatomy): success, rootless_path = ( @@ -21,7 +23,9 @@ def remap_source(source, anatomy): ).format(source)) return source -def get_skeleton_instance() + +def get_skeleton_instance(): + """ instance_skeleton_data = { "family": family, "subset": subset, @@ -46,4 +50,6 @@ def get_skeleton_instance() "useSequenceForReview": data.get("useSequenceForReview", True), # map inputVersions `ObjectId` -> `str` so json supports it "inputVersions": list(map(str, data.get("inputVersions", []))) - } \ No newline at end of file + } + """ + pass From 56f404e8f9a841d67c9af478ca3aa1ea8df98337 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jan 2023 17:21:28 +0100 Subject: [PATCH 012/446] :rotating_light: hound fixes 2 --- .../plugins/publish/create_publish_royalrender_job.py | 2 +- openpype/pipeline/farm/pyblish.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index e62289641b..65de600bfa 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -21,7 +21,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "celaction": [r".*"]} def process(self, instance): - data = instance.data.copy() + # data = instance.data.copy() context = instance.context self.context = context self.anatomy = instance.context.data["anatomy"] diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish.py index 15f4356b86..436ad7f195 100644 --- a/openpype/pipeline/farm/pyblish.py +++ b/openpype/pipeline/farm/pyblish.py @@ -17,10 +17,9 @@ def remap_source(source, anatomy): else: # `rootless_path` is not set to `source` if none of roots match log = Logger.get_logger("farm_publishing") - log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues." - ).format(source)) + log.warning( + ("Could not find root path for remapping \"{}\"." + " This may cause issues.").format(source)) return source From 61fe2ac36b2b8167554239c41d6e23eac051a638 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 7 Feb 2023 19:22:54 +0100 Subject: [PATCH 013/446] :construction: work on publish job --- openpype/modules/royalrender/api.py | 11 +- .../publish/create_maya_royalrender_job.py | 1 - .../publish/create_publish_royalrender_job.py | 102 ++++++++---------- openpype/modules/royalrender/rr_job.py | 46 +++++++- 4 files changed, 94 insertions(+), 66 deletions(-) diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index 86d27ccc6c..e610a0c8a8 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -20,19 +20,19 @@ class Api: self._rr_path = rr_path os.environ["RR_ROOT"] = rr_path - def _get_rr_bin_path(self, tool_name=None, rr_root=None): + @staticmethod + def get_rr_bin_path(rr_root, tool_name=None): # type: (str, str) -> str """Get path to RR bin folder. Args: tool_name (str): Name of RR executable you want. - rr_root (str, Optional): Custom RR root if needed. + rr_root (str): Custom RR root if needed. Returns: str: Path to the tool based on current platform. """ - rr_root = rr_root or self._rr_path is_64bit_python = sys.maxsize > 2 ** 32 rr_bin_parts = [rr_root, "bin"] @@ -65,7 +65,7 @@ class Api: # type: () -> None """Set RR modules for Python.""" # default for linux - rr_bin = self._get_rr_bin_path() + rr_bin = self.get_rr_bin_path(self._rr_path) rr_module_path = os.path.join(rr_bin, "lx64/lib") if sys.platform.lower() == "win32": @@ -109,7 +109,8 @@ class Api: def _submit_using_console(self, job_file): # type: (SubmitFile) -> None - rr_start_local = self._get_rr_bin_path("rrStartLocal") + rr_start_local = self.get_rr_bin_path( + self._rr_path, "rrStartLocal") self.log.info("rr_console: {}".format(rr_start_local)) diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 5f427353ac..0b257d8b7a 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -32,7 +32,6 @@ class CreateMayaRoyalRenderJob(InstancePlugin): Returns: RRJob: RoyalRender job payload. - """ def get_rr_platform(): if sys.platform.lower() in ["win32", "win64"]: diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 65de600bfa..f87ee589b6 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -6,6 +6,10 @@ from openpype.pipeline import legacy_io import requests import os +from openpype.modules.royalrender.rr_job import RRJob, RREnvList +from openpype.pipeline.publish import KnownPublishError +from openpype.modules.royalrender.api import Api as rrApi + class CreatePublishRoyalRenderJob(InstancePlugin): label = "Create publish job in RR" @@ -45,14 +49,12 @@ class CreatePublishRoyalRenderJob(InstancePlugin): ).format(source)) return source - def _submit_post_job(self, instance, job, instances): + def get_job(self, instance, job, instances): """Submit publish job to RoyalRender.""" data = instance.data.copy() subset = data["subset"] job_name = "Publish - {subset}".format(subset=subset) - # instance.data.get("subset") != instances[0]["subset"] - # 'Main' vs 'renderMain' override_version = None instance_version = instance.data.get("version") # take this if exists if instance_version != 1: @@ -62,6 +64,8 @@ class CreatePublishRoyalRenderJob(InstancePlugin): deepcopy(instance.data["anatomyData"]), instance.data.get("asset"), instances[0]["subset"], + # TODO: this shouldn't be hardcoded and is in fact settable by + # Settings. 'render', override_version ) @@ -71,7 +75,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): metadata_path, roothless_metadata_path = \ self._create_metadata_path(instance) - environment = { + environment = RREnvList({ "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], "AVALON_ASSET": legacy_io.Session["AVALON_ASSET"], "AVALON_TASK": legacy_io.Session["AVALON_TASK"], @@ -80,7 +84,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "OPENPYPE_RENDER_JOB": "0", "OPENPYPE_REMOTE_JOB": "0", "OPENPYPE_LOG_NO_COLORS": "1" - } + }) # add environments from self.environ_keys for env_key in self.environ_keys: @@ -88,7 +92,16 @@ class CreatePublishRoyalRenderJob(InstancePlugin): environment[env_key] = os.environ[env_key] # pass environment keys from self.environ_job_filter - job_environ = job["Props"].get("Env", {}) + # and collect all pre_ids to wait for + job_environ = {} + jobs_pre_ids = [] + for job in instance["rrJobs"]: # type: RRJob + if job.rrEnvList: + job_environ.update( + dict(RREnvList.parse(job.rrEnvList)) + ) + jobs_pre_ids.append(job.PreID) + for env_j_key in self.environ_job_filter: if job_environ.get(env_j_key): environment[env_j_key] = job_environ[env_j_key] @@ -99,7 +112,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): if mongo_url: environment["OPENPYPE_MONGO"] = mongo_url - priority = self.deadline_priority or instance.data.get("priority", 50) + priority = self.priority or instance.data.get("priority", 50) args = [ "--headless", @@ -109,66 +122,37 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "--targets", "farm" ] - # Generate the payload for Deadline submission - payload = { - "JobInfo": { - "Plugin": self.deadline_plugin, - "BatchName": job["Props"]["Batch"], - "Name": job_name, - "UserName": job["Props"]["User"], - "Comment": instance.context.data.get("comment", ""), - - "Department": self.deadline_department, - "ChunkSize": self.deadline_chunk_size, - "Priority": priority, - - "Group": self.deadline_group, - "Pool": instance.data.get("primaryPool"), - "SecondaryPool": instance.data.get("secondaryPool"), - - "OutputDirectory0": output_dir - }, - "PluginInfo": { - "Version": self.plugin_pype_version, - "Arguments": " ".join(args), - "SingleFrameOnly": "True", - }, - # Mandatory for Deadline, may be empty - "AuxFiles": [], - } + job = RRJob( + Software="Execute", + Renderer="Once", + # path to OpenPype + SeqStart=1, + SeqEnd=1, + SeqStep=1, + SeqFileOffset=0, + Version="1.0", + SceneName="", + IsActive=True, + ImageFilename="execOnce.file", + ImageDir="", + ImageExtension="", + ImagePreNumberLetter="", + SceneOS=RRJob.get_rr_platform(), + rrEnvList=environment.serialize(), + Priority=priority + ) # add assembly jobs as dependencies if instance.data.get("tileRendering"): self.log.info("Adding tile assembly jobs as dependencies...") - job_index = 0 - for assembly_id in instance.data.get("assemblySubmissionJobs"): - payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 - job_index += 1 + job.WaitForPreIDs += instance.data.get("assemblySubmissionJobs") elif instance.data.get("bakingSubmissionJobs"): self.log.info("Adding baking submission jobs as dependencies...") - job_index = 0 - for assembly_id in instance.data["bakingSubmissionJobs"]: - payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 - job_index += 1 + job.WaitForPreIDs += instance.data["bakingSubmissionJobs"] else: - payload["JobInfo"]["JobDependency0"] = job["_id"] + job.WaitForPreIDs += jobs_pre_ids - if instance.data.get("suspend_publish"): - payload["JobInfo"]["InitialStatus"] = "Suspended" - - for index, (key_, value_) in enumerate(environment.items()): - payload["JobInfo"].update( - { - "EnvironmentKeyValue%d" - % index: "{key}={value}".format( - key=key_, value=value_ - ) - } - ) - # remove secondary pool - payload["JobInfo"].pop("SecondaryPool", None) - - self.log.info("Submitting Deadline job ...") + self.log.info("Creating RoyalRender Publish job ...") url = "{}/api/jobs".format(self.deadline_url) response = requests.post(url, json=payload, timeout=10) diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index f5c7033b62..21e5291bc3 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Python wrapper for RoyalRender XML job file.""" +import sys from xml.dom import minidom as md import attr from collections import namedtuple, OrderedDict @@ -8,6 +9,23 @@ from collections import namedtuple, OrderedDict CustomAttribute = namedtuple("CustomAttribute", ["name", "value"]) +class RREnvList(dict): + def serialize(self): + # VariableA=ValueA~~~VariableB=ValueB + return "~~~".join( + ["{}={}".format(k, v) for k, v in sorted(self.items())]) + + @staticmethod + def parse(data): + # type: (str) -> RREnvList + """Parse rrEnvList string and return it as RREnvList object.""" + out = RREnvList() + for var in data.split("~~~"): + k, v = data.split("=") + out[k] = v + return out + + @attr.s class RRJob: """Mapping of Royal Render job file to a data class.""" @@ -108,7 +126,7 @@ class RRJob: # jobs send from this machine. If a job with the PreID was found, then # this jobs waits for the other job. Note: This flag can be used multiple # times to wait for multiple jobs. - WaitForPreID = attr.ib(default=None) # type: int + WaitForPreIDs = attr.ib(factory=list) # type: list # List of submitter options per job # list item must be of `SubmitterParameter` type @@ -138,6 +156,21 @@ class RRJob: TotalFrames = attr.ib(default=None) # type: int Tiled = attr.ib(default=None) # type: str + # Environment + # only used in RR 8.3 and newer + rrEnvList = attr.ib(default=None) # type: str + + @staticmethod + def get_rr_platform(): + # type: () -> str + """Returns name of platform used in rr jobs.""" + if sys.platform.lower() in ["win32", "win64"]: + return "windows" + elif sys.platform.lower() == "darwin": + return "mac" + else: + return "linux" + class SubmitterParameter: """Wrapper for Submitter Parameters.""" @@ -242,6 +275,8 @@ class SubmitFile: job, dict_factory=OrderedDict, filter=filter_data) serialized_job.pop("CustomAttributes") serialized_job.pop("SubmitterParameters") + # we are handling `WaitForPreIDs` separately. + wait_pre_ids = serialized_job.pop("WaitForPreIDs", []) for custom_attr in job_custom_attributes: # type: CustomAttribute serialized_job["Custom{}".format( @@ -253,6 +288,15 @@ class SubmitFile: root.createTextNode(str(value)) ) xml_job.appendChild(xml_attr) + + # WaitForPreID - can be used multiple times + for pre_id in wait_pre_ids: + xml_attr = root.createElement("WaitForPreID") + xml_attr.appendChild( + root.createTextNode(str(pre_id)) + ) + xml_job.appendChild(xml_attr) + job_file.appendChild(xml_job) return root.toprettyxml(indent="\t") From fbf1b9e6592dac486730253cd9b82ea8ea61be86 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 7 Mar 2023 17:24:58 +0100 Subject: [PATCH 014/446] :art: simple bgeo publishing in houdini --- .../houdini/plugins/create/create_bgeo.py | 65 +++++++++++++++++++ .../houdini/plugins/publish/collect_frames.py | 7 +- .../houdini/plugins/publish/extract_bgeo.py | 50 ++++++++++++++ openpype/plugins/publish/integrate.py | 3 +- 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/create/create_bgeo.py create mode 100644 openpype/hosts/houdini/plugins/publish/extract_bgeo.py diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py new file mode 100644 index 0000000000..38e63dbc9f --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating pointcache bgeo files.""" +from openpype.hosts.houdini.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.lib import EnumDef + + +class CreateBGEO(plugin.HoudiniCreator): + """BGEO pointcache creator.""" + identifier = "io.openpype.creators.houdini.bgeo" + label = "BGEO PointCache" + family = "bgeo" + icon = "gears" + + def create(self, subset_name, instance_data, pre_create_data): + import hou + + instance_data.pop("active", None) + + instance_data.update({"node_type": "geometry"}) + instance_data["bgeo_type"] = pre_create_data.get("bgeo_type") + + instance = super(CreateBGEO, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + instance_node = hou.node(instance.get("instance_node")) + + file_path = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.{}".format( + subset_name, + pre_create_data.get("bgeo_type") or "bgeo.sc") + ) + parms = { + "sopoutput": file_path + } + + if self.selected_nodes: + parms["soppath"] = self.selected_nodes[0].path() + + # try to find output node + for child in self.selected_nodes[0].children(): + if child.type().name() == "output": + parms["soppath"] = child.path() + break + + instance_node.setParms(parms) + instance_node.parm("trange").set(1) + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + bgeo_enum = [ + {"option": "bgeo", "value": "bgeo", "label": "uncompressed bgeo (.bgeo)"}, + {"option": "bgeosc", "value": "bgeosc", "label": "BLOSC compressed bgeo (.bgeosc)"}, + {"option": "bgeo.sc", "value": "bgeo.sc", "label": "BLOSC compressed bgeo (.bgeo.sc)"}, + {"option": "bgeo.gz", "value": "bgeo.gz", "label": "GZ compressed bgeo (.bgeo.gz)"}, + {"option": "bgeo.lzma", "value": "bgeo.lzma", "label": "LZMA compressed bgeo (.bgeo.lzma)"}, + {"option": "bgeo.bz2", "value": "bgeo.bz2", "label": "BZip2 compressed bgeo (.bgeo.bz2)"} + ] + + return attrs + [ + EnumDef("bgeo_type", bgeo_enum, label="BGEO Options"), + ] diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 531cdf1249..f0264b10a6 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -8,13 +8,12 @@ import pyblish.api from openpype.hosts.houdini.api import lib - class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" order = pyblish.api.CollectorOrder label = "Collect Frames" - families = ["vdbcache", "imagesequence", "ass", "redshiftproxy"] + families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "bgeo"] def process(self, instance): @@ -35,7 +34,9 @@ class CollectFrames(pyblish.api.InstancePlugin): output = output_parm.eval() _, ext = lib.splitext(output, - allowed_multidot_extensions=[".ass.gz"]) + allowed_multidot_extensions=[ + ".ass.gz", ".bgeo.sc", ".bgeo.gz", + ".bgeo.lzma", ".bgeo.bz2"]) file_name = os.path.basename(output) result = file_name diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py new file mode 100644 index 0000000000..8b14ca7418 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -0,0 +1,50 @@ +import os + +import pyblish.api + +from openpype.pipeline import publish +from openpype.hosts.houdini.api.lib import render_rop + +import hou + + +class ExtractBGEO(publish.Extractor): + + order = pyblish.api.ExtractorOrder + label = "Extract BGEO" + hosts = ["houdini"] + families = ["bgeo"] + + def process(self, instance): + + ropnode = hou.node(instance.data["instance_node"]) + + # Get the filename from the filename parameter + output = ropnode.evalParm("sopoutput") + staging_dir = os.path.dirname(output) + instance.data["stagingDir"] = staging_dir + + file_name = os.path.basename(output) + + # We run the render + self.log.info("Writing bgeo files '%s' to '%s'" % (file_name, + staging_dir)) + + # write files + ropnode.parm("execute").pressButton() + + output = instance.data["frames"] + self.log.debug(f"output: {output}") + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'bgeo', + 'ext': instance.data["bgeo_type"], + 'files': output, + "stagingDir": staging_dir, + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"] + } + instance.data["representations"].append(representation) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index b117006871..0971459c0c 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -132,7 +132,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "mvUsdOverride", "simpleUnrealTexture", "online", - "uasset" + "uasset", + "bgeo" ] default_template_name = "publish" From b2a936a7902be9392cb81ee01715145226e3596a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 8 Mar 2023 16:48:14 +0100 Subject: [PATCH 015/446] :rotating_light: few fixes --- openpype/hosts/houdini/plugins/create/create_bgeo.py | 12 ++++++------ .../hosts/houdini/plugins/publish/collect_frames.py | 6 +++--- .../hosts/houdini/plugins/publish/extract_bgeo.py | 7 +++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index 38e63dbc9f..1ca2ce36a0 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -52,12 +52,12 @@ class CreateBGEO(plugin.HoudiniCreator): def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() bgeo_enum = [ - {"option": "bgeo", "value": "bgeo", "label": "uncompressed bgeo (.bgeo)"}, - {"option": "bgeosc", "value": "bgeosc", "label": "BLOSC compressed bgeo (.bgeosc)"}, - {"option": "bgeo.sc", "value": "bgeo.sc", "label": "BLOSC compressed bgeo (.bgeo.sc)"}, - {"option": "bgeo.gz", "value": "bgeo.gz", "label": "GZ compressed bgeo (.bgeo.gz)"}, - {"option": "bgeo.lzma", "value": "bgeo.lzma", "label": "LZMA compressed bgeo (.bgeo.lzma)"}, - {"option": "bgeo.bz2", "value": "bgeo.bz2", "label": "BZip2 compressed bgeo (.bgeo.bz2)"} + {"value": "bgeo", "label": "uncompressed bgeo (.bgeo)"}, + {"value": "bgeosc", "label": "BLOSC compressed bgeo (.bgeosc)"}, + {"value": "bgeo.sc", "label": "BLOSC compressed bgeo (.bgeo.sc)"}, + {"value": "bgeo.gz", "label": "GZ compressed bgeo (.bgeo.gz)"}, + {"value": "bgeo.lzma", "label": "LZMA compressed bgeo (.bgeo.lzma)"}, + {"value": "bgeo.bz2", "label": "BZip2 compressed bgeo (.bgeo.bz2)"} ] return attrs + [ diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index f0264b10a6..0c2915552a 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -33,8 +33,8 @@ class CollectFrames(pyblish.api.InstancePlugin): self.log.warning("Using current frame: {}".format(hou.frame())) output = output_parm.eval() - _, ext = lib.splitext(output, - allowed_multidot_extensions=[ + _, ext = lib.splitext( + output, allowed_multidot_extensions=[ ".ass.gz", ".bgeo.sc", ".bgeo.gz", ".bgeo.lzma", ".bgeo.bz2"]) file_name = os.path.basename(output) @@ -78,7 +78,7 @@ class CollectFrames(pyblish.api.InstancePlugin): frame = match.group(1) padding = len(frame) - # Get the parts of the filename surrounding the frame number + # Get the parts of the filename surrounding the frame number, # so we can put our own frame numbers in. span = match.span(1) prefix = match.string[: span[0]] diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py index 8b14ca7418..5e022bc9c0 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -27,14 +27,13 @@ class ExtractBGEO(publish.Extractor): file_name = os.path.basename(output) # We run the render - self.log.info("Writing bgeo files '%s' to '%s'" % (file_name, - staging_dir)) + self.log.info("Writing bgeo files '{}' to '{}'.".format( + file_name, staging_dir)) # write files - ropnode.parm("execute").pressButton() + render_rop(ropnode) output = instance.data["frames"] - self.log.debug(f"output: {output}") if "representations" not in instance.data: instance.data["representations"] = [] From 6a3692b832b717fd90cc256497fa09169f6e87f4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 8 Mar 2023 16:55:01 +0100 Subject: [PATCH 016/446] :rotating_light: hound fixes :dog: --- .../houdini/plugins/create/create_bgeo.py | 30 +++++++++++++++---- .../houdini/plugins/publish/collect_frames.py | 4 +-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index 1ca2ce36a0..46fa47df92 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -52,12 +52,30 @@ class CreateBGEO(plugin.HoudiniCreator): def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() bgeo_enum = [ - {"value": "bgeo", "label": "uncompressed bgeo (.bgeo)"}, - {"value": "bgeosc", "label": "BLOSC compressed bgeo (.bgeosc)"}, - {"value": "bgeo.sc", "label": "BLOSC compressed bgeo (.bgeo.sc)"}, - {"value": "bgeo.gz", "label": "GZ compressed bgeo (.bgeo.gz)"}, - {"value": "bgeo.lzma", "label": "LZMA compressed bgeo (.bgeo.lzma)"}, - {"value": "bgeo.bz2", "label": "BZip2 compressed bgeo (.bgeo.bz2)"} + { + "value": "bgeo", + "label": "uncompressed bgeo (.bgeo)" + }, + { + "value": "bgeosc", + "label": "BLOSC compressed bgeo (.bgeosc)" + }, + { + "value": "bgeo.sc", + "label": "BLOSC compressed bgeo (.bgeo.sc)" + }, + { + "value": "bgeo.gz", + "label": "GZ compressed bgeo (.bgeo.gz)" + }, + { + "value": "bgeo.lzma", + "label": "LZMA compressed bgeo (.bgeo.lzma)" + }, + { + "value": "bgeo.bz2", + "label": "BZip2 compressed bgeo (.bgeo.bz2)" + } ] return attrs + [ diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 0c2915552a..6769915453 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -35,8 +35,8 @@ class CollectFrames(pyblish.api.InstancePlugin): _, ext = lib.splitext( output, allowed_multidot_extensions=[ - ".ass.gz", ".bgeo.sc", ".bgeo.gz", - ".bgeo.lzma", ".bgeo.bz2"]) + ".ass.gz", ".bgeo.sc", ".bgeo.gz", + ".bgeo.lzma", ".bgeo.bz2"]) file_name = os.path.basename(output) result = file_name From be39ab147c329b4bf2f27c33c4272733cba1ec66 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 8 Mar 2023 17:29:05 +0100 Subject: [PATCH 017/446] :memo: add docs --- website/docs/artist_hosts_houdini.md | 24 ++++++++++++++++++ website/docs/assets/houdini_bgeo-loading.png | Bin 0 -> 123822 bytes .../docs/assets/houdini_bgeo-publisher.png | Bin 0 -> 94696 bytes .../docs/assets/houdini_bgeo_output_node.png | Bin 0 -> 11699 bytes 4 files changed, 24 insertions(+) create mode 100644 website/docs/assets/houdini_bgeo-loading.png create mode 100644 website/docs/assets/houdini_bgeo-publisher.png create mode 100644 website/docs/assets/houdini_bgeo_output_node.png diff --git a/website/docs/artist_hosts_houdini.md b/website/docs/artist_hosts_houdini.md index f2b128ffc6..30648974e9 100644 --- a/website/docs/artist_hosts_houdini.md +++ b/website/docs/artist_hosts_houdini.md @@ -101,3 +101,27 @@ switch versions between different hda types. When you load hda, it will install its type in your hip file and add published version as its definition file. When you switch version via Scene Manager, it will add its definition and set it as preferred. + +## Publishing and loading BGEO caches + +There is a simple support for publishing and loading **BGEO** files in all supported compression variants. + +### Creating BGEO instances + +Just select your object to be exported as BGEO. If there is `output` node inside, first found will be used as entry +point: + +![BGEO output node](assets/houdini_bgeo_output_node.png) + +Then you can open Publisher, in Create you select **BGEO PointCache**: + +![BGEO Publisher](assets/houdini_bgeo-publisher.png) + +You can select compression type and if the current selection should be connected to ROPs SOP path parameter. Publishing +will produce sequence of files based on your timeline settings. + +### Loading BGEO + +Select your published BGEO subsets in Loader, right click and load them in: + +![BGEO Publisher](assets/houdini_bgeo-loading.png) diff --git a/website/docs/assets/houdini_bgeo-loading.png b/website/docs/assets/houdini_bgeo-loading.png new file mode 100644 index 0000000000000000000000000000000000000000..e8aad66f43980da0135fa4371e3eaa00f50dd0ad GIT binary patch literal 123822 zcmc$F1yGi4*DWQT(wz#@-Q9we@DS4R5YpWsjijg#NQ3l4cZYPRba#Uwo#)2)eZTKJ z|Cu>+{xfst%%IHejCWku-fOSD_PRc*D$8M@lAyxCz+k+VmwpQa^9&OP25uG^4)_<( zStc*w7p(JJIZ2q3VX|G|0>MH;Ndg9@JQnTF_!)4G;vlc%3Q|k!}BBPvvjUbD9!;0zn&5X0Wg_c<>Rh z@DblsIWjT=i8+kK@i?-cBlJ;8$k-mTFLF&7zs*bB-AzmLYfY~9)+tGaHMYN+{O#$X zdH;SA3aZwcG@#T39i}s*-T5O2QvLI@@`(`_K?3`)pHA!l+bagOO}2@paTH_Mf8Uzj zQXYcxugf)_|M}``;b~ZbSYgV^WqqcYKKUJ=j)Z@u*qXmV(#a*8`h))aUb6qVnTg&> z(A?|ZJK5_MtJ_5VK1pnvR�NgQny~xJ@tsFssx^N*jQdySS_ltf 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 Y``j!d+-hon^cr}bR#sPfs$g>KUwMz%8~^|S literal 0 HcmV?d00001 diff --git a/website/docs/assets/houdini_bgeo-publisher.png b/website/docs/assets/houdini_bgeo-publisher.png new file mode 100644 index 0000000000000000000000000000000000000000..5c3534077f37a4025096beed579bbfcc9b444023 GIT binary patch literal 94696 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%0D=#<7{jwP*ke{Ow&PKHPUfotBM<@I9^+u6q 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/houdini_bgeo_output_node.png b/website/docs/assets/houdini_bgeo_output_node.png new file mode 100644 index 0000000000000000000000000000000000000000..160f0a259b98e830b98997cba281f903a571a552 GIT binary patch literal 11699 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 literal 0 HcmV?d00001 From 912c34aca03f98b826d75ed73e5052f7d363c448 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 8 Mar 2023 17:44:39 +0100 Subject: [PATCH 018/446] :memo: :bug: fix file name bug preventing building of documentation --- website/docs/artist_hosts_aftereffects.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index a9c9ca49fa..32cc8af232 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -15,18 +15,18 @@ sidebar_label: AfterEffects ## Setup -To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. +To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. -Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. +Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. ## Implemented functionality AfterEffects implementation currently allows you to import and add various media to composition (image plates, renders, audio files, video files etc.) -and send prepared composition for rendering to Deadline or render locally. +and send prepared composition for rendering to Deadline or render locally. ## Usage -When you launch AfterEffects you will be met with the Workfiles app. If don't +When you launch AfterEffects you will be met with the Workfiles app. If don't have any previous workfiles, you can just close this window. Workfiles tools takes care of saving your .AEP files in the correct location and under @@ -34,7 +34,7 @@ a correct name. You should use it instead of standard file saving dialog. In AfterEffects you'll find the tools in the `OpenPype` extension: -![Extension](assets/photoshop_extension.PNG) +![Extension](assets/photoshop_extension.png) You can show the extension panel by going to `Window` > `Extensions` > `OpenPype`. @@ -67,7 +67,7 @@ Publisher allows publishing into different context, just click on any instance, #### RenderQueue -AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. +AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. Currently its expected to have only single render item per composition in the Render Queue. From 68edd86cd68be165ba0a0cc83367d8fe6e2a02a8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 15 Mar 2023 14:47:44 +0100 Subject: [PATCH 019/446] :rotating_light: fix quotes style --- .../hosts/houdini/plugins/publish/extract_bgeo.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py index 5e022bc9c0..daa35adfff 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -5,6 +5,14 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop +import hou +import os + +import pyblish.api + +from openpype.pipeline import publish +from openpype.hosts.houdini.api.lib import render_rop + import hou @@ -39,9 +47,9 @@ class ExtractBGEO(publish.Extractor): instance.data["representations"] = [] representation = { - 'name': 'bgeo', - 'ext': instance.data["bgeo_type"], - 'files': output, + "name": "bgeo", + "ext": instance.data["bgeo_type"], + "files": output, "stagingDir": staging_dir, "frameStart": instance.data["frameStart"], "frameEnd": instance.data["frameEnd"] From af471f6bb3ed236e0b80cde34abe2d00493baed0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 15 Mar 2023 14:48:53 +0100 Subject: [PATCH 020/446] :bug: fix messed up imports --- openpype/hosts/houdini/plugins/publish/extract_bgeo.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py index daa35adfff..23c3b78813 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -5,14 +5,6 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop -import hou -import os - -import pyblish.api - -from openpype.pipeline import publish -from openpype.hosts.houdini.api.lib import render_rop - import hou From 085d803558f894261b5cf24c19b95a2f8a4557d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 26 Oct 2022 18:37:44 +0200 Subject: [PATCH 021/446] :construction: wip on maya royalrender submit plugin --- openpype/modules/royalrender/api.py | 20 ++- .../publish/submit_maya_royalrender.py | 150 ++++++++++++++++++ openpype/modules/royalrender/rr_job.py | 8 +- 3 files changed, 163 insertions(+), 15 deletions(-) create mode 100644 openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index de1dba8724..c47d50b62b 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -15,9 +15,8 @@ class Api: RR_SUBMIT_CONSOLE = 1 RR_SUBMIT_API = 2 - def __init__(self, settings, project=None): + def __init__(self, project=None): self.log = Logger.get_logger("RoyalRender") - self._settings = settings self._initialize_rr(project) def _initialize_rr(self, project=None): @@ -91,21 +90,21 @@ class Api: sys.path.append(os.path.join(self._rr_path, rr_module_path)) - def create_submission(self, jobs, submitter_attributes, file_name=None): - # type: (list[RRJob], list[SubmitterParameter], str) -> SubmitFile + @staticmethod + def create_submission(jobs, submitter_attributes): + # type: (list[RRJob], list[SubmitterParameter]) -> SubmitFile """Create jobs submission file. Args: jobs (list): List of :class:`RRJob` submitter_attributes (list): List of submitter attributes :class:`SubmitterParameter` for whole submission batch. - file_name (str), optional): File path to write data to. Returns: str: XML data of job submission files. """ - raise NotImplementedError + return SubmitFile(SubmitterParameters=submitter_attributes, Jobs=jobs) def submit_file(self, file, mode=RR_SUBMIT_CONSOLE): # type: (SubmitFile, int) -> None @@ -119,15 +118,14 @@ class Api: # self._submit_using_api(file) def _submit_using_console(self, file): - # type: (SubmitFile) -> bool + # type: (SubmitFile) -> None rr_console = os.path.join( self._get_rr_bin_path(), - "rrSubmitterconsole" + "rrSubmitterConsole" ) - if sys.platform.lower() == "darwin": - if "/bin/mac64" in rr_console: - rr_console = rr_console.replace("/bin/mac64", "/bin/mac") + if sys.platform.lower() == "darwin" and "/bin/mac64" in rr_console: + rr_console = rr_console.replace("/bin/mac64", "/bin/mac") if sys.platform.lower() == "win32": if "/bin/win64" in rr_console: diff --git a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py new file mode 100644 index 0000000000..c354cc80a0 --- /dev/null +++ b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +"""Submitting render job to RoyalRender.""" +import os +import sys +import tempfile + +from maya.OpenMaya import MGlobal # noqa +from pyblish.api import InstancePlugin, IntegratorOrder +from openpype.hosts.maya.api.lib import get_attr_in_layer +from openpype.pipeline.farm.tools import get_published_workfile_instance +from openpype.pipeline.publish import KnownPublishError +from openpype.modules.royalrender.api import Api as rr_api +from openpype.modules.royalrender.rr_job import RRJob, SubmitterParameter + + +class MayaSubmitRoyalRender(InstancePlugin): + label = "Submit to RoyalRender" + order = IntegratorOrder + 0.1 + use_published = True + + def __init__(self, *args, **kwargs): + self._instance = None + self._rrRoot = None + self.scene_path = None + self.job = None + self.submission_parameters = None + self.rr_api = None + + def get_job(self): + """Prepare job payload. + + Returns: + RRJob: RoyalRender job payload. + + """ + def get_rr_platform(): + if sys.platform.lower() in ["win32", "win64"]: + return "win" + elif sys.platform.lower() == "darwin": + return "mac" + else: + return "lx" + + expected_files = self._instance.data["expectedFiles"] + first_file = next(self._iter_expected_files(expected_files)) + output_dir = os.path.dirname(first_file) + self._instance.data["outputDir"] = output_dir + workspace = self._instance.context.data["workspaceDir"] + default_render_file = self._instance.context.data.get('project_settings') \ + .get('maya') \ + .get('RenderSettings') \ + .get('default_render_image_folder') + filename = os.path.basename(self.scene_path) + dirname = os.path.join(workspace, default_render_file) + + job = RRJob( + Software="Maya", + Renderer=self._instance.data["renderer"], + SeqStart=int(self._instance.data["frameStartHandle"]), + SeqEnd=int(self._instance.data["frameEndHandle"]), + SeqStep=int(self._instance.data["byFrameStep"]), + SeqFileOffset=0, + Version="{0:.2f}".format(MGlobal.apiVersion() / 10000), + SceneName=os.path.basename(self.scene_path), + IsActive=True, + ImageDir=dirname, + ImageFilename=filename, + ImageExtension="." + os.path.splitext(filename)[1], + ImagePreNumberLetter=".", + ImageSingleOutputFile=False, + SceneOS=get_rr_platform(), + Camera=self._instance.data["cameras"][0], + Layer=self._instance.data["layer"], + SceneDatabaseDir=workspace, + ImageFramePadding=get_attr_in_layer( + "defaultRenderGlobals.extensionPadding", + self._instance.data["layer"]), + ImageWidth=self._instance.data["resolutionWidth"], + ImageHeight=self._instance.data["resolutionHeight"] + ) + return job + + @staticmethod + def get_submission_parameters(): + return [] + + def create_file(self, name, ext, contents=None): + temp = tempfile.NamedTemporaryFile( + dir=self.tempdir, + suffix=ext, + prefix=name + '.', + delete=False, + ) + + if contents: + with open(temp.name, 'w') as f: + f.write(contents) + + return temp.name + + def process(self, instance): + """Plugin entry point.""" + self._instance = instance + context = instance.context + self.rr_api = rr_api(context.data["project"]) + + # get royalrender module + """ + try: + rr_module = context.data.get( + "openPypeModules")["royalrender"] + except AttributeError: + self.log.error("Cannot get OpenPype RoyalRender module.") + raise AssertionError("OpenPype RoyalRender module not found.") + """ + + self._rrRoot = instance.data["rrPath"] or context.data["defaultRRPath"] # noqa + if not self._rrRoot: + raise KnownPublishError( + ("Missing RoyalRender root. " + "You need to configure RoyalRender module.")) + file_path = None + if self.use_published: + file_path = get_published_workfile_instance() + + # fallback if nothing was set + if not file_path: + self.log.warning("Falling back to workfile") + file_path = context.data["currentFile"] + + self.scene_path = file_path + self.job = self.get_job() + self.log.info(self.job) + self.submission_parameters = self.get_submission_parameters() + + self.process_submission() + + def process_submission(self): + submission = rr_api.create_submission( + [self.job], + self.submission_parameters) + + self.log.debug(submission) + xml = tempfile.NamedTemporaryFile(suffix=".xml", delete=False) + with open(xml.name, "w") as f: + f.write(submission.serialize()) + + self.rr_api.submit_file(file=xml) + + diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index c660eceac7..beb8c17187 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -35,7 +35,7 @@ class RRJob: # Is the job enabled for submission? # enabled by default - IsActive = attr.ib() # type: str + IsActive = attr.ib() # type: bool # Sequence settings of this job SeqStart = attr.ib() # type: int @@ -60,7 +60,7 @@ class RRJob: # If you render a single file, e.g. Quicktime or Avi, then you have to # set this value. Videos have to be rendered at once on one client. - ImageSingleOutputFile = attr.ib(default="false") # type: str + ImageSingleOutputFile = attr.ib(default=False) # type: bool # Semi-Required (required for some render applications) # ----------------------------------------------------- @@ -169,11 +169,11 @@ class SubmitFile: # Delete submission file after processing DeleteXML = attr.ib(default=1) # type: int - # List of submitter options per job + # List of the submitter options per job. # list item must be of `SubmitterParameter` type SubmitterParameters = attr.ib(factory=list) # type: list - # List of job is submission batch. + # List of the jobs in submission batch. # list item must be of type `RRJob` Jobs = attr.ib(factory=list) # type: list From 77315d301c79fe0125bcaab624a8e0ef405f312c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 26 Oct 2022 18:38:11 +0200 Subject: [PATCH 022/446] :recycle: move functions to common lib --- .../deadline/abstract_submit_deadline.py | 111 +----------------- openpype/pipeline/farm/tools.py | 109 +++++++++++++++++ 2 files changed, 111 insertions(+), 109 deletions(-) create mode 100644 openpype/pipeline/farm/tools.py diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 648eb77007..5d1b703b77 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -21,6 +21,7 @@ from openpype.pipeline.publish import ( AbstractMetaInstancePlugin, KnownPublishError ) +from openpype.pipeline.farm.tools import get_published_workfile_instance JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) @@ -426,7 +427,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): file_path = None if self.use_published: if not self.import_reference: - file_path = self.from_published_scene() + file_path = get_published_workfile_instance(instance) else: self.log.info("use the scene with imported reference for rendering") # noqa file_path = context.data["currentFile"] @@ -500,95 +501,6 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): """ return [] - def from_published_scene(self, replace_in_path=True): - """Switch work scene for published scene. - - If rendering/exporting from published scenes is enabled, this will - replace paths from working scene to published scene. - - Args: - replace_in_path (bool): if True, it will try to find - old scene name in path of expected files and replace it - with name of published scene. - - Returns: - str: Published scene path. - None: if no published scene is found. - - Note: - Published scene path is actually determined from project Anatomy - as at the time this plugin is running scene can still no be - published. - - """ - instance = self._instance - workfile_instance = self._get_workfile_instance(instance.context) - if workfile_instance is None: - return - - # determine published path from Anatomy. - template_data = workfile_instance.data.get("anatomyData") - rep = workfile_instance.data["representations"][0] - template_data["representation"] = rep.get("name") - template_data["ext"] = rep.get("ext") - template_data["comment"] = None - - anatomy = instance.context.data['anatomy'] - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] - file_path = os.path.normpath(template_filled) - - self.log.info("Using published scene for render {}".format(file_path)) - - if not os.path.exists(file_path): - self.log.error("published scene does not exist!") - raise - - if not replace_in_path: - return file_path - - # now we need to switch scene in expected files - # because token will now point to published - # scene file and that might differ from current one - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(file_path) - orig_scene = _clean_name(instance.context.data["currentFile"]) - expected_files = instance.data.get("expectedFiles") - - if isinstance(expected_files[0], dict): - # we have aovs and we need to iterate over them - new_exp = {} - for aov, files in expected_files[0].items(): - replaced_files = [] - for f in files: - replaced_files.append( - str(f).replace(orig_scene, new_scene) - ) - new_exp[aov] = replaced_files - # [] might be too much here, TODO - instance.data["expectedFiles"] = [new_exp] - else: - new_exp = [] - for f in expected_files: - new_exp.append( - str(f).replace(orig_scene, new_scene) - ) - instance.data["expectedFiles"] = new_exp - - metadata_folder = instance.data.get("publishRenderMetadataFolder") - if metadata_folder: - metadata_folder = metadata_folder.replace(orig_scene, - new_scene) - instance.data["publishRenderMetadataFolder"] = metadata_folder - - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) - - return file_path - def assemble_payload( self, job_info=None, plugin_info=None, aux_files=None): """Assemble payload data from its various parts. @@ -648,22 +560,3 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): self._instance.data["deadlineSubmissionJob"] = result return result["_id"] - - @staticmethod - def _get_workfile_instance(context): - """Find workfile instance in context""" - for i in context: - - is_workfile = ( - "workfile" in i.data.get("families", []) or - i.data["family"] == "workfile" - ) - if not is_workfile: - continue - - # test if there is instance of workfile waiting - # to be published. - assert i.data["publish"] is True, ( - "Workfile (scene) must be published along") - - return i diff --git a/openpype/pipeline/farm/tools.py b/openpype/pipeline/farm/tools.py new file mode 100644 index 0000000000..8cf1af399e --- /dev/null +++ b/openpype/pipeline/farm/tools.py @@ -0,0 +1,109 @@ +import os + + +def get_published_workfile_instance(context): + """Find workfile instance in context""" + for i in context: + is_workfile = ( + "workfile" in i.data.get("families", []) or + i.data["family"] == "workfile" + ) + if not is_workfile: + continue + + # test if there is instance of workfile waiting + # to be published. + if i.data["publish"] is not True: + continue + + return i + + +def from_published_scene(instance, replace_in_path=True): + """Switch work scene for published scene. + + If rendering/exporting from published scenes is enabled, this will + replace paths from working scene to published scene. + + Args: + instance (pyblish.api.Instance): Instance data to process. + replace_in_path (bool): if True, it will try to find + old scene name in path of expected files and replace it + with name of published scene. + + Returns: + str: Published scene path. + None: if no published scene is found. + + Note: + Published scene path is actually determined from project Anatomy + as at the time this plugin is running the scene can be still + un-published. + + """ + workfile_instance = get_published_workfile_instance(instance.context) + if workfile_instance is None: + return + + # determine published path from Anatomy. + template_data = workfile_instance.data.get("anatomyData") + rep = workfile_instance.data.get("representations")[0] + template_data["representation"] = rep.get("name") + template_data["ext"] = rep.get("ext") + template_data["comment"] = None + + anatomy = instance.context.data['anatomy'] + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled["publish"]["path"] + file_path = os.path.normpath(template_filled) + + self.log.info("Using published scene for render {}".format(file_path)) + + if not os.path.exists(file_path): + self.log.error("published scene does not exist!") + raise + + if not replace_in_path: + return file_path + + # now we need to switch scene in expected files + # because token will now point to published + # scene file and that might differ from current one + def _clean_name(path): + return os.path.splitext(os.path.basename(path))[0] + + new_scene = _clean_name(file_path) + orig_scene = _clean_name(instance.context.data["currentFile"]) + expected_files = instance.data.get("expectedFiles") + + if isinstance(expected_files[0], dict): + # we have aovs and we need to iterate over them + new_exp = {} + for aov, files in expected_files[0].items(): + replaced_files = [] + for f in files: + replaced_files.append( + str(f).replace(orig_scene, new_scene) + ) + new_exp[aov] = replaced_files + # [] might be too much here, TODO + instance.data["expectedFiles"] = [new_exp] + else: + new_exp = [] + for f in expected_files: + new_exp.append( + str(f).replace(orig_scene, new_scene) + ) + instance.data["expectedFiles"] = new_exp + + metadata_folder = instance.data.get("publishRenderMetadataFolder") + if metadata_folder: + metadata_folder = metadata_folder.replace(orig_scene, + new_scene) + instance.data["publishRenderMetadataFolder"] = metadata_folder + + self.log.info("Scene name was switched {} -> {}".format( + orig_scene, new_scene + )) + + return file_path From ca552a701816b2d0fec6c1ee282fbb89f79b5db9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 11 Jan 2023 19:13:28 +0100 Subject: [PATCH 023/446] :construction: redoing publishing flow for multiple rr roots --- .../maya/plugins/create/create_render.py | 89 +++++++++++++++---- .../maya/plugins/publish/collect_render.py | 7 ++ openpype/modules/royalrender/api.py | 30 +------ .../publish/collect_default_rr_path.py | 23 ----- .../publish/collect_rr_path_from_instance.py | 16 ++-- .../publish/collect_sequences_from_job.py | 2 +- .../publish/submit_maya_royalrender.py | 51 +++++++++-- .../project_settings/royalrender.json | 3 + .../defaults/system_settings/modules.json | 6 +- 9 files changed, 138 insertions(+), 89 deletions(-) delete mode 100644 openpype/modules/royalrender/plugins/publish/collect_default_rr_path.py diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 387b7321b9..337868d47d 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -80,31 +80,58 @@ class CreateRender(plugin.Creator): if self._project_settings["maya"]["RenderSettings"]["apply_render_settings"]: # noqa lib_rendersettings.RenderSettings().set_default_renderer_settings() - # Deadline-only + # Handling farms manager = ModulesManager() deadline_settings = get_system_settings()["modules"]["deadline"] - if not deadline_settings["enabled"]: - self.deadline_servers = {} + rr_settings = get_system_settings()["modules"]["royalrender"] + + self.deadline_servers = {} + self.rr_paths = {} + + if deadline_settings["enabled"]: + self.deadline_module = manager.modules_by_name["deadline"] + try: + default_servers = deadline_settings["deadline_urls"] + project_servers = ( + self._project_settings["deadline"]["deadline_servers"] + ) + self.deadline_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + + if not self.deadline_servers: + self.deadline_servers = default_servers + + except AttributeError: + # Handle situation were we had only one url for deadline. + # get default deadline webservice url from deadline module + self.deadline_servers = self.deadline_module.deadline_urls + + # RoyalRender only + if not rr_settings["enabled"]: return - self.deadline_module = manager.modules_by_name["deadline"] + + self.rr_module = manager.modules_by_name["royalrender"] try: - default_servers = deadline_settings["deadline_urls"] - project_servers = ( - self._project_settings["deadline"]["deadline_servers"] + default_paths = rr_settings["rr_paths"] + project_paths = ( + self._project_settings["royalrender"]["rr_paths"] ) - self.deadline_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers + self.rr_paths = { + k: default_paths[k] + for k in project_paths + if k in default_paths } - if not self.deadline_servers: - self.deadline_servers = default_servers + if not self.rr_paths: + self.rr_paths = default_paths except AttributeError: - # Handle situation were we had only one url for deadline. - # get default deadline webservice url from deadline module - self.deadline_servers = self.deadline_module.deadline_urls + # Handle situation were we had only one path for royalrender. + # Get default royalrender root path from the rr module. + self.rr_paths = self.rr_module.rr_paths def process(self): """Entry point.""" @@ -140,6 +167,14 @@ class CreateRender(plugin.Creator): self._deadline_webservice_changed ]) + # add RoyalRender root path selection list + if self.rr_paths: + cmds.scriptJob( + attributeChange=[ + "{}.rrPaths".format(self.instance), + self._rr_path_changed + ]) + cmds.setAttr("{}.machineList".format(self.instance), lock=True) rs = renderSetup.instance() layers = rs.getRenderLayers() @@ -192,6 +227,18 @@ class CreateRender(plugin.Creator): attributeType="enum", enumName=":".join(sorted_pools)) + @staticmethod + def _rr_path_changed(): + """Unused callback to pull information from RR.""" + """ + _ = self.rr_paths[ + self.server_aliases[ + cmds.getAttr("{}.rrPaths".format(self.instance)) + ] + ] + """ + pass + def _create_render_settings(self): """Create instance settings.""" # get pools (slave machines of the render farm) @@ -226,15 +273,21 @@ class CreateRender(plugin.Creator): system_settings = get_system_settings()["modules"] deadline_enabled = system_settings["deadline"]["enabled"] + royalrender_enabled = system_settings["royalrender"]["enabled"] muster_enabled = system_settings["muster"]["enabled"] muster_url = system_settings["muster"]["MUSTER_REST_URL"] - if deadline_enabled and muster_enabled: + if deadline_enabled and muster_enabled and royalrender_enabled: self.log.error( - "Both Deadline and Muster are enabled. " "Cannot support both." + ("Multiple render farm support (Deadline/RoyalRender/Muster) " + "is enabled. We support only one at time.") ) raise RuntimeError("Both Deadline and Muster are enabled") + if royalrender_enabled: + self.server_aliases = list(self.rr_paths.keys()) + self.data["rrPaths"] = self.server_aliases + if deadline_enabled: self.server_aliases = list(self.deadline_servers.keys()) self.data["deadlineServers"] = self.server_aliases diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 7c47f17acb..2fb55782d2 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -338,6 +338,13 @@ class CollectMayaRender(pyblish.api.ContextPlugin): if deadline_settings["enabled"]: data["deadlineUrl"] = render_instance.data.get("deadlineUrl") + rr_settings = ( + context.data["system_settings"]["modules"]["royalrender"] + ) + if rr_settings["enabled"]: + data["rrPathName"] = render_instance.data.get("rrPathName") + self.log.info(data["rrPathName"]) + if self.sync_workfile_version: data["version"] = context.data["version"] diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index c47d50b62b..dcb518deb1 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -15,36 +15,10 @@ class Api: RR_SUBMIT_CONSOLE = 1 RR_SUBMIT_API = 2 - def __init__(self, project=None): + def __init__(self, rr_path=None): self.log = Logger.get_logger("RoyalRender") - self._initialize_rr(project) - - def _initialize_rr(self, project=None): - # type: (str) -> None - """Initialize RR Path. - - Args: - project (str, Optional): Project name to set RR api in - context. - - """ - if project: - project_settings = get_project_settings(project) - rr_path = ( - project_settings - ["royalrender"] - ["rr_paths"] - ) - else: - rr_path = ( - self._settings - ["modules"] - ["royalrender"] - ["rr_path"] - ["default"] - ) - os.environ["RR_ROOT"] = rr_path self._rr_path = rr_path + os.environ["RR_ROOT"] = rr_path def _get_rr_bin_path(self, rr_root=None): # type: (str) -> str diff --git a/openpype/modules/royalrender/plugins/publish/collect_default_rr_path.py b/openpype/modules/royalrender/plugins/publish/collect_default_rr_path.py deleted file mode 100644 index 3ce95e0c50..0000000000 --- a/openpype/modules/royalrender/plugins/publish/collect_default_rr_path.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect default Deadline server.""" -import pyblish.api - - -class CollectDefaultRRPath(pyblish.api.ContextPlugin): - """Collect default Royal Render path.""" - - order = pyblish.api.CollectorOrder - label = "Default Royal Render Path" - - def process(self, context): - try: - rr_module = context.data.get( - "openPypeModules")["royalrender"] - except AttributeError: - msg = "Cannot get OpenPype Royal Render module." - self.log.error(msg) - raise AssertionError(msg) - - # get default deadline webservice url from deadline module - self.log.debug(rr_module.rr_paths) - context.data["defaultRRPath"] = rr_module.rr_paths["default"] # noqa: E501 diff --git a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index 6a3dc276f3..187e2b9c44 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -5,19 +5,19 @@ import pyblish.api class CollectRRPathFromInstance(pyblish.api.InstancePlugin): """Collect RR Path from instance.""" - order = pyblish.api.CollectorOrder + 0.01 - label = "Royal Render Path from the Instance" + order = pyblish.api.CollectorOrder + label = "Collect Royal Render path name from the Instance" families = ["rendering"] def process(self, instance): - instance.data["rrPath"] = self._collect_rr_path(instance) + instance.data["rrPathName"] = self._collect_rr_path_name(instance) self.log.info( - "Using {} for submission.".format(instance.data["rrPath"])) + "Using '{}' for submission.".format(instance.data["rrPathName"])) @staticmethod - def _collect_rr_path(render_instance): + def _collect_rr_path_name(render_instance): # type: (pyblish.api.Instance) -> str - """Get Royal Render path from render instance.""" + """Get Royal Render pat name from render instance.""" rr_settings = ( render_instance.context.data ["system_settings"] @@ -42,8 +42,6 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): # Handle situation were we had only one url for royal render. return render_instance.context.data["defaultRRPath"] - return rr_servers[ - list(rr_servers.keys())[ + return list(rr_servers.keys())[ int(render_instance.data.get("rrPaths")) ] - ] diff --git a/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py b/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py index 65af90e8a6..4c123e4134 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py +++ b/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py @@ -71,7 +71,7 @@ class CollectSequencesFromJob(pyblish.api.ContextPlugin): """Gather file sequences from job directory. When "OPENPYPE_PUBLISH_DATA" environment variable is set these paths - (folders or .json files) are parsed for image sequences. Otherwise the + (folders or .json files) are parsed for image sequences. Otherwise, the current working directory is searched for file sequences. """ diff --git a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py index c354cc80a0..784e4c5ff9 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py @@ -3,9 +3,10 @@ import os import sys import tempfile +import platform from maya.OpenMaya import MGlobal # noqa -from pyblish.api import InstancePlugin, IntegratorOrder +from pyblish.api import InstancePlugin, IntegratorOrder, Context from openpype.hosts.maya.api.lib import get_attr_in_layer from openpype.pipeline.farm.tools import get_published_workfile_instance from openpype.pipeline.publish import KnownPublishError @@ -16,6 +17,8 @@ from openpype.modules.royalrender.rr_job import RRJob, SubmitterParameter class MayaSubmitRoyalRender(InstancePlugin): label = "Submit to RoyalRender" order = IntegratorOrder + 0.1 + families = ["renderlayer"] + targets = ["local"] use_published = True def __init__(self, *args, **kwargs): @@ -102,7 +105,16 @@ class MayaSubmitRoyalRender(InstancePlugin): """Plugin entry point.""" self._instance = instance context = instance.context - self.rr_api = rr_api(context.data["project"]) + from pprint import pformat + + self._rr_root = self._resolve_rr_path(context, instance.data.get("rrPathName")) # noqa + self.log.debug(self._rr_root) + if not self._rr_root: + raise KnownPublishError( + ("Missing RoyalRender root. " + "You need to configure RoyalRender module.")) + + self.rr_api = rr_api(self._rr_root) # get royalrender module """ @@ -114,11 +126,7 @@ class MayaSubmitRoyalRender(InstancePlugin): raise AssertionError("OpenPype RoyalRender module not found.") """ - self._rrRoot = instance.data["rrPath"] or context.data["defaultRRPath"] # noqa - if not self._rrRoot: - raise KnownPublishError( - ("Missing RoyalRender root. " - "You need to configure RoyalRender module.")) + file_path = None if self.use_published: file_path = get_published_workfile_instance() @@ -147,4 +155,33 @@ class MayaSubmitRoyalRender(InstancePlugin): self.rr_api.submit_file(file=xml) + @staticmethod + def _resolve_rr_path(context, rr_path_name): + # type: (Context, str) -> str + rr_settings = ( + context.data + ["system_settings"] + ["modules"] + ["royalrender"] + ) + try: + default_servers = rr_settings["rr_paths"] + project_servers = ( + context.data + ["project_settings"] + ["royalrender"] + ["rr_paths"] + ) + rr_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + + except (AttributeError, KeyError): + # Handle situation were we had only one url for royal render. + return context.data["defaultRRPath"][platform.system().lower()] + + return rr_servers[rr_path_name][platform.system().lower()] + diff --git a/openpype/settings/defaults/project_settings/royalrender.json b/openpype/settings/defaults/project_settings/royalrender.json index b72fed8474..14e36058aa 100644 --- a/openpype/settings/defaults/project_settings/royalrender.json +++ b/openpype/settings/defaults/project_settings/royalrender.json @@ -1,4 +1,7 @@ { + "rr_paths": [ + "default" + ], "publish": { "CollectSequencesFromJob": { "review": true diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 1ddbfd2726..f524f01d45 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -185,9 +185,9 @@ "enabled": false, "rr_paths": { "default": { - "windows": "", - "darwin": "", - "linux": "" + "windows": "C:\\RR8", + "darwin": "/Volumes/share/RR8", + "linux": "/mnt/studio/RR8" } } }, From 7e4b3cb3af9041c6e43d329410469fc4dbae1632 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 11 Jan 2023 19:13:54 +0100 Subject: [PATCH 024/446] :recycle: optimizing enum classes --- openpype/settings/entities/__init__.py | 4 +- openpype/settings/entities/enum_entity.py | 87 ++++++++++--------- .../schema_project_royalrender.json | 6 ++ 3 files changed, 56 insertions(+), 41 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 5e3a76094e..00db2b33a7 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -107,7 +107,8 @@ from .enum_entity import ( TaskTypeEnumEntity, DeadlineUrlEnumEntity, AnatomyTemplatesEnumEntity, - ShotgridUrlEnumEntity + ShotgridUrlEnumEntity, + RoyalRenderRootEnumEntity ) from .list_entity import ListEntity @@ -170,6 +171,7 @@ __all__ = ( "TaskTypeEnumEntity", "DeadlineUrlEnumEntity", "ShotgridUrlEnumEntity", + "RoyalRenderRootEnumEntity", "AnatomyTemplatesEnumEntity", "ListEntity", diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index c0c103ea10..9f9ae93026 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -1,3 +1,5 @@ +import abc +import six import copy from .input_entities import InputEntity from .exceptions import EntitySchemaError @@ -476,8 +478,9 @@ class TaskTypeEnumEntity(BaseEnumEntity): self.set(value_on_not_set) -class DeadlineUrlEnumEntity(BaseEnumEntity): - schema_types = ["deadline_url-enum"] +@six.add_metaclass(abc.ABCMeta) +class FarmRootEnumEntity(BaseEnumEntity): + schema_types = [] def _item_initialization(self): self.multiselection = self.schema_data.get("multiselection", True) @@ -495,22 +498,8 @@ class DeadlineUrlEnumEntity(BaseEnumEntity): # GUI attribute self.placeholder = self.schema_data.get("placeholder") - def _get_enum_values(self): - deadline_urls_entity = self.get_entity_from_path( - "system_settings/modules/deadline/deadline_urls" - ) - - valid_keys = set() - enum_items_list = [] - for server_name, url_entity in deadline_urls_entity.items(): - enum_items_list.append( - {server_name: "{}: {}".format(server_name, url_entity.value)} - ) - valid_keys.add(server_name) - return enum_items_list, valid_keys - def set_override_state(self, *args, **kwargs): - super(DeadlineUrlEnumEntity, self).set_override_state(*args, **kwargs) + super(FarmRootEnumEntity, self).set_override_state(*args, **kwargs) self.enum_items, self.valid_keys = self._get_enum_values() if self.multiselection: @@ -527,22 +516,50 @@ class DeadlineUrlEnumEntity(BaseEnumEntity): elif self._current_value not in self.valid_keys: self._current_value = tuple(self.valid_keys)[0] + @abc.abstractmethod + def _get_enum_values(self): + pass -class ShotgridUrlEnumEntity(BaseEnumEntity): + +class DeadlineUrlEnumEntity(FarmRootEnumEntity): + schema_types = ["deadline_url-enum"] + + def _get_enum_values(self): + deadline_urls_entity = self.get_entity_from_path( + "system_settings/modules/deadline/deadline_urls" + ) + + valid_keys = set() + enum_items_list = [] + for server_name, url_entity in deadline_urls_entity.items(): + enum_items_list.append( + {server_name: "{}: {}".format(server_name, url_entity.value)} + ) + valid_keys.add(server_name) + return enum_items_list, valid_keys + + +class RoyalRenderRootEnumEntity(FarmRootEnumEntity): + schema_types = ["rr_root-enum"] + + def _get_enum_values(self): + rr_root_entity = self.get_entity_from_path( + "system_settings/modules/royalrender/rr_paths" + ) + + valid_keys = set() + enum_items_list = [] + for server_name, url_entity in rr_root_entity.items(): + enum_items_list.append( + {server_name: "{}: {}".format(server_name, url_entity.value)} + ) + valid_keys.add(server_name) + return enum_items_list, valid_keys + + +class ShotgridUrlEnumEntity(FarmRootEnumEntity): schema_types = ["shotgrid_url-enum"] - def _item_initialization(self): - self.multiselection = False - - self.enum_items = [] - self.valid_keys = set() - - self.valid_value_types = (STRING_TYPE,) - self.value_on_not_set = "" - - # GUI attribute - self.placeholder = self.schema_data.get("placeholder") - def _get_enum_values(self): shotgrid_settings = self.get_entity_from_path( "system_settings/modules/shotgrid/shotgrid_settings" @@ -561,16 +578,6 @@ class ShotgridUrlEnumEntity(BaseEnumEntity): valid_keys.add(server_name) return enum_items_list, valid_keys - def set_override_state(self, *args, **kwargs): - super(ShotgridUrlEnumEntity, self).set_override_state(*args, **kwargs) - - self.enum_items, self.valid_keys = self._get_enum_values() - if not self.valid_keys: - self._current_value = "" - - elif self._current_value not in self.valid_keys: - self._current_value = tuple(self.valid_keys)[0] - class AnatomyTemplatesEnumEntity(BaseEnumEntity): schema_types = ["anatomy-templates-enum"] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_royalrender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_royalrender.json index cabb4747d5..f4bf2f51ba 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_royalrender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_royalrender.json @@ -5,6 +5,12 @@ "collapsible": true, "is_file": true, "children": [ + { + "type": "rr_root-enum", + "key": "rr_paths", + "label": "Royal Render Roots", + "multiselect": true + }, { "type": "dict", "collapsible": true, From 7d212c051b34ad505dae4132903bc4b2f94998fd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 16 Jan 2023 10:40:05 +0100 Subject: [PATCH 025/446] :construction: render job submission --- openpype/modules/royalrender/api.py | 76 +++++++++++-------- .../publish/submit_maya_royalrender.py | 45 ++++++----- openpype/modules/royalrender/rr_job.py | 10 ++- 3 files changed, 77 insertions(+), 54 deletions(-) diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index dcb518deb1..8b13b9781f 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -2,11 +2,13 @@ """Wrapper around Royal Render API.""" import sys import os +import platform from openpype.settings import get_project_settings from openpype.lib.local_settings import OpenPypeSettingsRegistry from openpype.lib import Logger, run_subprocess from .rr_job import RRJob, SubmitFile, SubmitterParameter +from openpype.lib.vendor_bin_utils import find_tool_in_custom_paths class Api: @@ -20,31 +22,46 @@ class Api: self._rr_path = rr_path os.environ["RR_ROOT"] = rr_path - def _get_rr_bin_path(self, rr_root=None): - # type: (str) -> str - """Get path to RR bin folder.""" + def _get_rr_bin_path(self, tool_name=None, rr_root=None): + # type: (str, str) -> str + """Get path to RR bin folder. + + Args: + tool_name (str): Name of RR executable you want. + rr_root (str, Optional): Custom RR root if needed. + + Returns: + str: Path to the tool based on current platform. + + """ rr_root = rr_root or self._rr_path is_64bit_python = sys.maxsize > 2 ** 32 - rr_bin_path = "" + rr_bin_parts = [rr_root, "bin"] if sys.platform.lower() == "win32": - rr_bin_path = "/bin/win64" - if not is_64bit_python: - # we are using 64bit python - rr_bin_path = "/bin/win" - rr_bin_path = rr_bin_path.replace( - "/", os.path.sep - ) + rr_bin_parts.append("win") if sys.platform.lower() == "darwin": - rr_bin_path = "/bin/mac64" - if not is_64bit_python: - rr_bin_path = "/bin/mac" + rr_bin_parts.append("mac") - if sys.platform.lower() == "linux": - rr_bin_path = "/bin/lx64" + if sys.platform.lower().startswith("linux"): + rr_bin_parts.append("lx") - return os.path.join(rr_root, rr_bin_path) + rr_bin_path = os.sep.join(rr_bin_parts) + + paths_to_check = [] + # if we use 64bit python, append 64bit specific path first + if is_64bit_python: + if not tool_name: + return rr_bin_path + "64" + paths_to_check.append(rr_bin_path + "64") + + # otherwise use 32bit + if not tool_name: + return rr_bin_path + paths_to_check.append(rr_bin_path) + + return find_tool_in_custom_paths(paths_to_check, tool_name) def _initialize_module_path(self): # type: () -> None @@ -84,30 +101,25 @@ class Api: # type: (SubmitFile, int) -> None if mode == self.RR_SUBMIT_CONSOLE: self._submit_using_console(file) + return - # RR v7 supports only Python 2.7 so we bail out in fear + # RR v7 supports only Python 2.7, so we bail out in fear # until there is support for Python 3 😰 raise NotImplementedError( "Submission via RoyalRender API is not supported yet") # self._submit_using_api(file) - def _submit_using_console(self, file): + def _submit_using_console(self, job_file): # type: (SubmitFile) -> None - rr_console = os.path.join( - self._get_rr_bin_path(), - "rrSubmitterConsole" - ) + rr_start_local = self._get_rr_bin_path("rrStartLocal") - if sys.platform.lower() == "darwin" and "/bin/mac64" in rr_console: - rr_console = rr_console.replace("/bin/mac64", "/bin/mac") + self.log.info("rr_console: {}".format(rr_start_local)) - if sys.platform.lower() == "win32": - if "/bin/win64" in rr_console: - rr_console = rr_console.replace("/bin/win64", "/bin/win") - rr_console += ".exe" - - args = [rr_console, file] - run_subprocess(" ".join(args), logger=self.log) + args = [rr_start_local, "rrSubmitterconsole", job_file] + self.log.info("Executing: {}".format(" ".join(args))) + env = os.environ + env["RR_ROOT"] = self._rr_path + run_subprocess(args, logger=self.log, env=env) def _submit_using_api(self, file): # type: (SubmitFile) -> None diff --git a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py index 784e4c5ff9..82236386b7 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py @@ -38,11 +38,11 @@ class MayaSubmitRoyalRender(InstancePlugin): """ def get_rr_platform(): if sys.platform.lower() in ["win32", "win64"]: - return "win" + return "windows" elif sys.platform.lower() == "darwin": return "mac" else: - return "lx" + return "linux" expected_files = self._instance.data["expectedFiles"] first_file = next(self._iter_expected_files(expected_files)) @@ -53,8 +53,10 @@ class MayaSubmitRoyalRender(InstancePlugin): .get('maya') \ .get('RenderSettings') \ .get('default_render_image_folder') - filename = os.path.basename(self.scene_path) - dirname = os.path.join(workspace, default_render_file) + file_name = os.path.basename(self.scene_path) + dir_name = os.path.join(workspace, default_render_file) + layer = self._instance.data["setMembers"] # type: str + layer_name = layer.removeprefix("rs_") job = RRJob( Software="Maya", @@ -64,22 +66,22 @@ class MayaSubmitRoyalRender(InstancePlugin): SeqStep=int(self._instance.data["byFrameStep"]), SeqFileOffset=0, Version="{0:.2f}".format(MGlobal.apiVersion() / 10000), - SceneName=os.path.basename(self.scene_path), + SceneName=self.scene_path, IsActive=True, - ImageDir=dirname, - ImageFilename=filename, - ImageExtension="." + os.path.splitext(filename)[1], + ImageDir=dir_name, + ImageFilename="{}.".format(layer_name), + ImageExtension=os.path.splitext(first_file)[1], ImagePreNumberLetter=".", ImageSingleOutputFile=False, SceneOS=get_rr_platform(), Camera=self._instance.data["cameras"][0], - Layer=self._instance.data["layer"], + Layer=layer_name, SceneDatabaseDir=workspace, - ImageFramePadding=get_attr_in_layer( - "defaultRenderGlobals.extensionPadding", - self._instance.data["layer"]), + CustomSHotName=self._instance.context.data["asset"], + CompanyProjectName=self._instance.context.data["projectName"], ImageWidth=self._instance.data["resolutionWidth"], - ImageHeight=self._instance.data["resolutionHeight"] + ImageHeight=self._instance.data["resolutionHeight"], + PreID=1 ) return job @@ -125,11 +127,9 @@ class MayaSubmitRoyalRender(InstancePlugin): self.log.error("Cannot get OpenPype RoyalRender module.") raise AssertionError("OpenPype RoyalRender module not found.") """ - - file_path = None if self.use_published: - file_path = get_published_workfile_instance() + file_path = get_published_workfile_instance(context) # fallback if nothing was set if not file_path: @@ -153,7 +153,8 @@ class MayaSubmitRoyalRender(InstancePlugin): with open(xml.name, "w") as f: f.write(submission.serialize()) - self.rr_api.submit_file(file=xml) + self.log.info("submitting job file: {}".format(xml.name)) + self.rr_api.submit_file(file=xml.name) @staticmethod def _resolve_rr_path(context, rr_path_name): @@ -184,4 +185,12 @@ class MayaSubmitRoyalRender(InstancePlugin): return rr_servers[rr_path_name][platform.system().lower()] - + @staticmethod + def _iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index beb8c17187..f5c7033b62 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -87,7 +87,7 @@ class RRJob: # Frame Padding of the frame number in the rendered filename. # Some render config files are setting the padding at render time. - ImageFramePadding = attr.ib(default=None) # type: str + ImageFramePadding = attr.ib(default=None) # type: int # Some render applications support overriding the image format at # the render commandline. @@ -129,6 +129,7 @@ class RRJob: CustomUserInfo = attr.ib(default=None) # type: str SubmitMachine = attr.ib(default=None) # type: str Color_ID = attr.ib(default=2) # type: int + CompanyProjectName = attr.ib(default=None) # type: str RequiredLicenses = attr.ib(default=None) # type: str @@ -225,7 +226,7 @@ class SubmitFile: # foo=bar~baz~goo self._process_submitter_parameters( self.SubmitterParameters, root, job_file) - + root.appendChild(job_file) for job in self.Jobs: # type: RRJob if not isinstance(job, RRJob): raise AttributeError( @@ -247,10 +248,11 @@ class SubmitFile: custom_attr.name)] = custom_attr.value for item, value in serialized_job.items(): - xml_attr = root.create(item) + xml_attr = root.createElement(item) xml_attr.appendChild( - root.createTextNode(value) + root.createTextNode(str(value)) ) xml_job.appendChild(xml_attr) + job_file.appendChild(xml_job) return root.toprettyxml(indent="\t") From 7ab4e0f77362e36226acd6e340c1c924c4c4773c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jan 2023 17:06:05 +0100 Subject: [PATCH 026/446] :construction: refactor RR job flow --- .../publish/collect_rr_path_from_instance.py | 12 +- .../publish/create_maya_royalrender_job.py | 150 ++++++++++++++ .../publish/create_publish_royalrender_job.py | 178 ++++++++++++++++ .../publish/submit_jobs_to_royalrender.py | 115 ++++++++++ .../publish/submit_maya_royalrender.py | 196 ------------------ openpype/pipeline/farm/pyblish.py | 49 +++++ 6 files changed, 499 insertions(+), 201 deletions(-) create mode 100644 openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py create mode 100644 openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py create mode 100644 openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py create mode 100644 openpype/pipeline/farm/pyblish.py diff --git a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index 187e2b9c44..40f34561fa 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -15,19 +15,21 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): "Using '{}' for submission.".format(instance.data["rrPathName"])) @staticmethod - def _collect_rr_path_name(render_instance): + def _collect_rr_path_name(instance): # type: (pyblish.api.Instance) -> str """Get Royal Render pat name from render instance.""" rr_settings = ( - render_instance.context.data + instance.context.data ["system_settings"] ["modules"] ["royalrender"] ) + if not instance.data.get("rrPaths"): + return "default" try: default_servers = rr_settings["rr_paths"] project_servers = ( - render_instance.context.data + instance.context.data ["project_settings"] ["royalrender"] ["rr_paths"] @@ -40,8 +42,8 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): except (AttributeError, KeyError): # Handle situation were we had only one url for royal render. - return render_instance.context.data["defaultRRPath"] + return rr_settings["rr_paths"]["default"] return list(rr_servers.keys())[ - int(render_instance.data.get("rrPaths")) + int(instance.data.get("rrPaths")) ] diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py new file mode 100644 index 0000000000..e194a7edc6 --- /dev/null +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +"""Submitting render job to RoyalRender.""" +import os +import sys +import platform + +from maya.OpenMaya import MGlobal # noqa +from pyblish.api import InstancePlugin, IntegratorOrder, Context +from openpype.pipeline.publish.lib import get_published_workfile_instance +from openpype.pipeline.publish import KnownPublishError +from openpype.modules.royalrender.api import Api as rrApi +from openpype.modules.royalrender.rr_job import RRJob + + +class CreateMayaRoyalRenderJob(InstancePlugin): + label = "Create Maya Render job in RR" + order = IntegratorOrder + 0.1 + families = ["renderlayer"] + targets = ["local"] + use_published = True + + def __init__(self, *args, **kwargs): + self._instance = None + self._rrRoot = None + self.scene_path = None + self.job = None + self.submission_parameters = None + self.rr_api = None + + def get_job(self): + """Prepare job payload. + + Returns: + RRJob: RoyalRender job payload. + + """ + def get_rr_platform(): + if sys.platform.lower() in ["win32", "win64"]: + return "windows" + elif sys.platform.lower() == "darwin": + return "mac" + else: + return "linux" + + expected_files = self._instance.data["expectedFiles"] + first_file = next(self._iter_expected_files(expected_files)) + output_dir = os.path.dirname(first_file) + self._instance.data["outputDir"] = output_dir + workspace = self._instance.context.data["workspaceDir"] + default_render_file = self._instance.context.data.get('project_settings') \ + .get('maya') \ + .get('RenderSettings') \ + .get('default_render_image_folder') + file_name = os.path.basename(self.scene_path) + dir_name = os.path.join(workspace, default_render_file) + layer = self._instance.data["setMembers"] # type: str + layer_name = layer.removeprefix("rs_") + + job = RRJob( + Software="Maya", + Renderer=self._instance.data["renderer"], + SeqStart=int(self._instance.data["frameStartHandle"]), + SeqEnd=int(self._instance.data["frameEndHandle"]), + SeqStep=int(self._instance.data["byFrameStep"]), + SeqFileOffset=0, + Version="{0:.2f}".format(MGlobal.apiVersion() / 10000), + SceneName=self.scene_path, + IsActive=True, + ImageDir=dir_name, + ImageFilename="{}.".format(layer_name), + ImageExtension=os.path.splitext(first_file)[1], + ImagePreNumberLetter=".", + ImageSingleOutputFile=False, + SceneOS=get_rr_platform(), + Camera=self._instance.data["cameras"][0], + Layer=layer_name, + SceneDatabaseDir=workspace, + CustomSHotName=self._instance.context.data["asset"], + CompanyProjectName=self._instance.context.data["projectName"], + ImageWidth=self._instance.data["resolutionWidth"], + ImageHeight=self._instance.data["resolutionHeight"], + ) + return job + + def process(self, instance): + """Plugin entry point.""" + self._instance = instance + context = instance.context + from pprint import pformat + + self._rr_root = self._resolve_rr_path(context, instance.data.get("rrPathName")) # noqa + self.log.debug(self._rr_root) + if not self._rr_root: + raise KnownPublishError( + ("Missing RoyalRender root. " + "You need to configure RoyalRender module.")) + + self.rr_api = rrApi(self._rr_root) + + file_path = None + if self.use_published: + file_path = get_published_workfile_instance(context) + + # fallback if nothing was set + if not file_path: + self.log.warning("Falling back to workfile") + file_path = context.data["currentFile"] + + self.scene_path = file_path + + self._instance.data["rrJobs"] = [self.get_job()] + + @staticmethod + def _iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file + + @staticmethod + def _resolve_rr_path(context, rr_path_name): + # type: (Context, str) -> str + rr_settings = ( + context.data + ["system_settings"] + ["modules"] + ["royalrender"] + ) + try: + default_servers = rr_settings["rr_paths"] + project_servers = ( + context.data + ["project_settings"] + ["royalrender"] + ["rr_paths"] + ) + rr_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + + except (AttributeError, KeyError): + # Handle situation were we had only one url for royal render. + return context.data["defaultRRPath"][platform.system().lower()] + + return rr_servers[rr_path_name][platform.system().lower()] diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py new file mode 100644 index 0000000000..9d8cc602c5 --- /dev/null +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +"""Create publishing job on RoyalRender.""" +from pyblish.api import InstancePlugin, IntegratorOrder +from copy import deepcopy +from openpype.pipeline import legacy_io +import requests +import os + + +class CreatePublishRoyalRenderJob(InstancePlugin): + label = "Create publish job in RR" + order = IntegratorOrder + 0.2 + icon = "tractor" + targets = ["local"] + hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"] + families = ["render.farm", "prerender.farm", + "renderlayer", "imagesequence", "vrayscene"] + aov_filter = {"maya": [r".*([Bb]eauty).*"], + "aftereffects": [r".*"], # for everything from AE + "harmony": [r".*"], # for everything from AE + "celaction": [r".*"]} + + def process(self, instance): + data = instance.data.copy() + context = instance.context + self.context = context + self.anatomy = instance.context.data["anatomy"] + + asset = data.get("asset") + subset = data.get("subset") + source = self._remap_source( + data.get("source") or context.data["source"]) + + + + def _remap_source(self, source): + success, rootless_path = ( + self.anatomy.find_root_template_from_path(source) + ) + if success: + source = rootless_path + else: + # `rootless_path` is not set to `source` if none of roots match + self.log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues." + ).format(source)) + return source + + def _submit_post_job(self, instance, job, instances): + """Submit publish job to RoyalRender.""" + data = instance.data.copy() + subset = data["subset"] + job_name = "Publish - {subset}".format(subset=subset) + + # instance.data.get("subset") != instances[0]["subset"] + # 'Main' vs 'renderMain' + override_version = None + instance_version = instance.data.get("version") # take this if exists + if instance_version != 1: + override_version = instance_version + output_dir = self._get_publish_folder( + instance.context.data['anatomy'], + deepcopy(instance.data["anatomyData"]), + instance.data.get("asset"), + instances[0]["subset"], + 'render', + override_version + ) + + # Transfer the environment from the original job to this dependent + # job, so they use the same environment + metadata_path, roothless_metadata_path = \ + self._create_metadata_path(instance) + + environment = { + "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], + "AVALON_ASSET": legacy_io.Session["AVALON_ASSET"], + "AVALON_TASK": legacy_io.Session["AVALON_TASK"], + "OPENPYPE_USERNAME": instance.context.data["user"], + "OPENPYPE_PUBLISH_JOB": "1", + "OPENPYPE_RENDER_JOB": "0", + "OPENPYPE_REMOTE_JOB": "0", + "OPENPYPE_LOG_NO_COLORS": "1" + } + + # add environments from self.environ_keys + for env_key in self.environ_keys: + if os.getenv(env_key): + environment[env_key] = os.environ[env_key] + + # pass environment keys from self.environ_job_filter + job_environ = job["Props"].get("Env", {}) + for env_j_key in self.environ_job_filter: + if job_environ.get(env_j_key): + environment[env_j_key] = job_environ[env_j_key] + + # Add mongo url if it's enabled + if instance.context.data.get("deadlinePassMongoUrl"): + mongo_url = os.environ.get("OPENPYPE_MONGO") + if mongo_url: + environment["OPENPYPE_MONGO"] = mongo_url + + priority = self.deadline_priority or instance.data.get("priority", 50) + + args = [ + "--headless", + 'publish', + roothless_metadata_path, + "--targets", "deadline", + "--targets", "farm" + ] + + # Generate the payload for Deadline submission + payload = { + "JobInfo": { + "Plugin": self.deadline_plugin, + "BatchName": job["Props"]["Batch"], + "Name": job_name, + "UserName": job["Props"]["User"], + "Comment": instance.context.data.get("comment", ""), + + "Department": self.deadline_department, + "ChunkSize": self.deadline_chunk_size, + "Priority": priority, + + "Group": self.deadline_group, + "Pool": instance.data.get("primaryPool"), + "SecondaryPool": instance.data.get("secondaryPool"), + + "OutputDirectory0": output_dir + }, + "PluginInfo": { + "Version": self.plugin_pype_version, + "Arguments": " ".join(args), + "SingleFrameOnly": "True", + }, + # Mandatory for Deadline, may be empty + "AuxFiles": [], + } + + # add assembly jobs as dependencies + if instance.data.get("tileRendering"): + self.log.info("Adding tile assembly jobs as dependencies...") + job_index = 0 + for assembly_id in instance.data.get("assemblySubmissionJobs"): + payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 + job_index += 1 + elif instance.data.get("bakingSubmissionJobs"): + self.log.info("Adding baking submission jobs as dependencies...") + job_index = 0 + for assembly_id in instance.data["bakingSubmissionJobs"]: + payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 + job_index += 1 + else: + payload["JobInfo"]["JobDependency0"] = job["_id"] + + if instance.data.get("suspend_publish"): + payload["JobInfo"]["InitialStatus"] = "Suspended" + + for index, (key_, value_) in enumerate(environment.items()): + payload["JobInfo"].update( + { + "EnvironmentKeyValue%d" + % index: "{key}={value}".format( + key=key_, value=value_ + ) + } + ) + # remove secondary pool + payload["JobInfo"].pop("SecondaryPool", None) + + self.log.info("Submitting Deadline job ...") + + url = "{}/api/jobs".format(self.deadline_url) + response = requests.post(url, json=payload, timeout=10) + if not response.ok: + raise Exception(response.text) \ No newline at end of file diff --git a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py new file mode 100644 index 0000000000..4fcf3a08bd --- /dev/null +++ b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +"""Submit jobs to RoyalRender.""" +import tempfile +import platform + +from pyblish.api import IntegratorOrder, ContextPlugin, Context +from openpype.modules.royalrender.api import RRJob, Api as rrApi +from openpype.pipeline.publish import KnownPublishError + + +class SubmitJobsToRoyalRender(ContextPlugin): + """Find all jobs, create submission XML and submit it to RoyalRender.""" + label = "Submit jobs to RoyalRender" + order = IntegratorOrder + 0.1 + targets = ["local"] + + def __init__(self): + super(SubmitJobsToRoyalRender, self).__init__() + self._rr_root = None + self._rr_api = None + self._submission_parameters = [] + + def process(self, context): + rr_settings = ( + context.data + ["system_settings"] + ["modules"] + ["royalrender"] + ) + + if rr_settings["enabled"] is not True: + self.log.warning("RoyalRender modules is disabled.") + return + + # iterate over all instances and try to find RRJobs + jobs = [] + for instance in context: + if isinstance(instance.data.get("rrJob"), RRJob): + jobs.append(instance.data.get("rrJob")) + if instance.data.get("rrJobs"): + if all(isinstance(job, RRJob) for job in instance.data.get("rrJobs")): + jobs += instance.data.get("rrJobs") + + if jobs: + self._rr_root = self._resolve_rr_path( + context, instance.data.get("rrPathName")) # noqa + if not self._rr_root: + raise KnownPublishError( + ("Missing RoyalRender root. " + "You need to configure RoyalRender module.")) + self._rr_api = rrApi(self._rr_root) + self._submission_parameters = self.get_submission_parameters() + self.process_submission(jobs) + return + + self.log.info("No RoyalRender jobs found") + + def process_submission(self, jobs): + # type: ([RRJob]) -> None + submission = rrApi.create_submission( + jobs, + self._submission_parameters) + + xml = tempfile.NamedTemporaryFile(suffix=".xml", delete=False) + with open(xml.name, "w") as f: + f.write(submission.serialize()) + + self.log.info("submitting job(s) file: {}".format(xml.name)) + self._rr_api.submit_file(file=xml.name) + + def create_file(self, name, ext, contents=None): + temp = tempfile.NamedTemporaryFile( + dir=self.tempdir, + suffix=ext, + prefix=name + '.', + delete=False, + ) + + if contents: + with open(temp.name, 'w') as f: + f.write(contents) + + return temp.name + + def get_submission_parameters(self): + return [] + + @staticmethod + def _resolve_rr_path(context, rr_path_name): + # type: (Context, str) -> str + rr_settings = ( + context.data + ["system_settings"] + ["modules"] + ["royalrender"] + ) + try: + default_servers = rr_settings["rr_paths"] + project_servers = ( + context.data + ["project_settings"] + ["royalrender"] + ["rr_paths"] + ) + rr_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + + except (AttributeError, KeyError): + # Handle situation were we had only one url for royal render. + return context.data["defaultRRPath"][platform.system().lower()] + + return rr_servers[rr_path_name][platform.system().lower()] diff --git a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py index 82236386b7..e69de29bb2 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py @@ -1,196 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submitting render job to RoyalRender.""" -import os -import sys -import tempfile -import platform - -from maya.OpenMaya import MGlobal # noqa -from pyblish.api import InstancePlugin, IntegratorOrder, Context -from openpype.hosts.maya.api.lib import get_attr_in_layer -from openpype.pipeline.farm.tools import get_published_workfile_instance -from openpype.pipeline.publish import KnownPublishError -from openpype.modules.royalrender.api import Api as rr_api -from openpype.modules.royalrender.rr_job import RRJob, SubmitterParameter - - -class MayaSubmitRoyalRender(InstancePlugin): - label = "Submit to RoyalRender" - order = IntegratorOrder + 0.1 - families = ["renderlayer"] - targets = ["local"] - use_published = True - - def __init__(self, *args, **kwargs): - self._instance = None - self._rrRoot = None - self.scene_path = None - self.job = None - self.submission_parameters = None - self.rr_api = None - - def get_job(self): - """Prepare job payload. - - Returns: - RRJob: RoyalRender job payload. - - """ - def get_rr_platform(): - if sys.platform.lower() in ["win32", "win64"]: - return "windows" - elif sys.platform.lower() == "darwin": - return "mac" - else: - return "linux" - - expected_files = self._instance.data["expectedFiles"] - first_file = next(self._iter_expected_files(expected_files)) - output_dir = os.path.dirname(first_file) - self._instance.data["outputDir"] = output_dir - workspace = self._instance.context.data["workspaceDir"] - default_render_file = self._instance.context.data.get('project_settings') \ - .get('maya') \ - .get('RenderSettings') \ - .get('default_render_image_folder') - file_name = os.path.basename(self.scene_path) - dir_name = os.path.join(workspace, default_render_file) - layer = self._instance.data["setMembers"] # type: str - layer_name = layer.removeprefix("rs_") - - job = RRJob( - Software="Maya", - Renderer=self._instance.data["renderer"], - SeqStart=int(self._instance.data["frameStartHandle"]), - SeqEnd=int(self._instance.data["frameEndHandle"]), - SeqStep=int(self._instance.data["byFrameStep"]), - SeqFileOffset=0, - Version="{0:.2f}".format(MGlobal.apiVersion() / 10000), - SceneName=self.scene_path, - IsActive=True, - ImageDir=dir_name, - ImageFilename="{}.".format(layer_name), - ImageExtension=os.path.splitext(first_file)[1], - ImagePreNumberLetter=".", - ImageSingleOutputFile=False, - SceneOS=get_rr_platform(), - Camera=self._instance.data["cameras"][0], - Layer=layer_name, - SceneDatabaseDir=workspace, - CustomSHotName=self._instance.context.data["asset"], - CompanyProjectName=self._instance.context.data["projectName"], - ImageWidth=self._instance.data["resolutionWidth"], - ImageHeight=self._instance.data["resolutionHeight"], - PreID=1 - ) - return job - - @staticmethod - def get_submission_parameters(): - return [] - - def create_file(self, name, ext, contents=None): - temp = tempfile.NamedTemporaryFile( - dir=self.tempdir, - suffix=ext, - prefix=name + '.', - delete=False, - ) - - if contents: - with open(temp.name, 'w') as f: - f.write(contents) - - return temp.name - - def process(self, instance): - """Plugin entry point.""" - self._instance = instance - context = instance.context - from pprint import pformat - - self._rr_root = self._resolve_rr_path(context, instance.data.get("rrPathName")) # noqa - self.log.debug(self._rr_root) - if not self._rr_root: - raise KnownPublishError( - ("Missing RoyalRender root. " - "You need to configure RoyalRender module.")) - - self.rr_api = rr_api(self._rr_root) - - # get royalrender module - """ - try: - rr_module = context.data.get( - "openPypeModules")["royalrender"] - except AttributeError: - self.log.error("Cannot get OpenPype RoyalRender module.") - raise AssertionError("OpenPype RoyalRender module not found.") - """ - file_path = None - if self.use_published: - file_path = get_published_workfile_instance(context) - - # fallback if nothing was set - if not file_path: - self.log.warning("Falling back to workfile") - file_path = context.data["currentFile"] - - self.scene_path = file_path - self.job = self.get_job() - self.log.info(self.job) - self.submission_parameters = self.get_submission_parameters() - - self.process_submission() - - def process_submission(self): - submission = rr_api.create_submission( - [self.job], - self.submission_parameters) - - self.log.debug(submission) - xml = tempfile.NamedTemporaryFile(suffix=".xml", delete=False) - with open(xml.name, "w") as f: - f.write(submission.serialize()) - - self.log.info("submitting job file: {}".format(xml.name)) - self.rr_api.submit_file(file=xml.name) - - @staticmethod - def _resolve_rr_path(context, rr_path_name): - # type: (Context, str) -> str - rr_settings = ( - context.data - ["system_settings"] - ["modules"] - ["royalrender"] - ) - try: - default_servers = rr_settings["rr_paths"] - project_servers = ( - context.data - ["project_settings"] - ["royalrender"] - ["rr_paths"] - ) - rr_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers - } - - except (AttributeError, KeyError): - # Handle situation were we had only one url for royal render. - return context.data["defaultRRPath"][platform.system().lower()] - - return rr_servers[rr_path_name][platform.system().lower()] - - @staticmethod - def _iter_expected_files(exp): - if isinstance(exp[0], dict): - for _aov, files in exp[0].items(): - for file in files: - yield file - else: - for file in exp: - yield file diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish.py new file mode 100644 index 0000000000..02535f4090 --- /dev/null +++ b/openpype/pipeline/farm/pyblish.py @@ -0,0 +1,49 @@ +from openpype.lib import Logger +import attr + + +@attr.s +class InstanceSkeleton(object): + family = attr.ib(factory=) + +def remap_source(source, anatomy): + success, rootless_path = ( + anatomy.find_root_template_from_path(source) + ) + if success: + source = rootless_path + else: + # `rootless_path` is not set to `source` if none of roots match + log = Logger.get_logger("farm_publishing") + log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues." + ).format(source)) + return source + +def get_skeleton_instance() + instance_skeleton_data = { + "family": family, + "subset": subset, + "families": families, + "asset": asset, + "frameStart": start, + "frameEnd": end, + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStartHandle": start - handle_start, + "frameEndHandle": end + handle_end, + "comment": instance.data["comment"], + "fps": fps, + "source": source, + "extendFrames": data.get("extendFrames"), + "overrideExistingFrame": data.get("overrideExistingFrame"), + "pixelAspect": data.get("pixelAspect", 1), + "resolutionWidth": data.get("resolutionWidth", 1920), + "resolutionHeight": data.get("resolutionHeight", 1080), + "multipartExr": data.get("multipartExr", False), + "jobBatchName": data.get("jobBatchName", ""), + "useSequenceForReview": data.get("useSequenceForReview", True), + # map inputVersions `ObjectId` -> `str` so json supports it + "inputVersions": list(map(str, data.get("inputVersions", []))) + } \ No newline at end of file From e49cbf34f64260bd3e9960338d84a7be8589af9e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jan 2023 17:06:53 +0100 Subject: [PATCH 027/446] :bug: fix default for ShotGrid this is fixing default value for ShotGrid server enumerator after code refactor done in this branch --- openpype/settings/defaults/project_settings/shotgrid.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/shotgrid.json b/openpype/settings/defaults/project_settings/shotgrid.json index 83b6f69074..0dcffed28a 100644 --- a/openpype/settings/defaults/project_settings/shotgrid.json +++ b/openpype/settings/defaults/project_settings/shotgrid.json @@ -1,6 +1,6 @@ { "shotgrid_project_id": 0, - "shotgrid_server": "", + "shotgrid_server": [], "event": { "enabled": false }, From 8ebef523bc2689b3b56fe5bbd947fdfec4285eb9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jan 2023 17:19:18 +0100 Subject: [PATCH 028/446] :rotating_light: hound fixes --- .../hosts/maya/plugins/create/create_render.py | 2 +- openpype/modules/royalrender/api.py | 2 -- .../publish/collect_rr_path_from_instance.py | 4 +--- .../plugins/publish/create_maya_royalrender_job.py | 14 ++++++++------ .../publish/create_publish_royalrender_job.py | 12 +++++------- .../plugins/publish/submit_jobs_to_royalrender.py | 4 +++- openpype/pipeline/farm/pyblish.py | 12 +++++++++--- 7 files changed, 27 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 337868d47d..71d42cc82f 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -279,7 +279,7 @@ class CreateRender(plugin.Creator): if deadline_enabled and muster_enabled and royalrender_enabled: self.log.error( - ("Multiple render farm support (Deadline/RoyalRender/Muster) " + ("Multiple render farm support (Deadline/RoyalRender/Muster) " "is enabled. We support only one at time.") ) raise RuntimeError("Both Deadline and Muster are enabled") diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index 8b13b9781f..86d27ccc6c 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -2,9 +2,7 @@ """Wrapper around Royal Render API.""" import sys import os -import platform -from openpype.settings import get_project_settings from openpype.lib.local_settings import OpenPypeSettingsRegistry from openpype.lib import Logger, run_subprocess from .rr_job import RRJob, SubmitFile, SubmitterParameter diff --git a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index 40f34561fa..cfb5b78077 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -44,6 +44,4 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): # Handle situation were we had only one url for royal render. return rr_settings["rr_paths"]["default"] - return list(rr_servers.keys())[ - int(instance.data.get("rrPaths")) - ] + return list(rr_servers.keys())[int(instance.data.get("rrPaths"))] diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index e194a7edc6..5f427353ac 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -47,11 +47,14 @@ class CreateMayaRoyalRenderJob(InstancePlugin): output_dir = os.path.dirname(first_file) self._instance.data["outputDir"] = output_dir workspace = self._instance.context.data["workspaceDir"] - default_render_file = self._instance.context.data.get('project_settings') \ - .get('maya') \ - .get('RenderSettings') \ - .get('default_render_image_folder') - file_name = os.path.basename(self.scene_path) + default_render_file = ( + self._instance.context.data + ['project_settings'] + ['maya'] + ['RenderSettings'] + ['default_render_image_folder'] + ) + # file_name = os.path.basename(self.scene_path) dir_name = os.path.join(workspace, default_render_file) layer = self._instance.data["setMembers"] # type: str layer_name = layer.removeprefix("rs_") @@ -86,7 +89,6 @@ class CreateMayaRoyalRenderJob(InstancePlugin): """Plugin entry point.""" self._instance = instance context = instance.context - from pprint import pformat self._rr_root = self._resolve_rr_path(context, instance.data.get("rrPathName")) # noqa self.log.debug(self._rr_root) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 9d8cc602c5..e62289641b 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -26,12 +26,10 @@ class CreatePublishRoyalRenderJob(InstancePlugin): self.context = context self.anatomy = instance.context.data["anatomy"] - asset = data.get("asset") - subset = data.get("subset") - source = self._remap_source( - data.get("source") or context.data["source"]) - - + # asset = data.get("asset") + # subset = data.get("subset") + # source = self._remap_source( + # data.get("source") or context.data["source"]) def _remap_source(self, source): success, rootless_path = ( @@ -175,4 +173,4 @@ class CreatePublishRoyalRenderJob(InstancePlugin): url = "{}/api/jobs".format(self.deadline_url) response = requests.post(url, json=payload, timeout=10) if not response.ok: - raise Exception(response.text) \ No newline at end of file + raise Exception(response.text) diff --git a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py index 4fcf3a08bd..325fb36993 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -38,7 +38,9 @@ class SubmitJobsToRoyalRender(ContextPlugin): if isinstance(instance.data.get("rrJob"), RRJob): jobs.append(instance.data.get("rrJob")) if instance.data.get("rrJobs"): - if all(isinstance(job, RRJob) for job in instance.data.get("rrJobs")): + if all( + isinstance(job, RRJob) + for job in instance.data.get("rrJobs")): jobs += instance.data.get("rrJobs") if jobs: diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish.py index 02535f4090..15f4356b86 100644 --- a/openpype/pipeline/farm/pyblish.py +++ b/openpype/pipeline/farm/pyblish.py @@ -4,7 +4,9 @@ import attr @attr.s class InstanceSkeleton(object): - family = attr.ib(factory=) + # family = attr.ib(factory=) + pass + def remap_source(source, anatomy): success, rootless_path = ( @@ -21,7 +23,9 @@ def remap_source(source, anatomy): ).format(source)) return source -def get_skeleton_instance() + +def get_skeleton_instance(): + """ instance_skeleton_data = { "family": family, "subset": subset, @@ -46,4 +50,6 @@ def get_skeleton_instance() "useSequenceForReview": data.get("useSequenceForReview", True), # map inputVersions `ObjectId` -> `str` so json supports it "inputVersions": list(map(str, data.get("inputVersions", []))) - } \ No newline at end of file + } + """ + pass From 5dda835a0dba359c70512e7c21a36548eb4072a0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jan 2023 17:21:28 +0100 Subject: [PATCH 029/446] :rotating_light: hound fixes 2 --- .../plugins/publish/create_publish_royalrender_job.py | 2 +- openpype/pipeline/farm/pyblish.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index e62289641b..65de600bfa 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -21,7 +21,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "celaction": [r".*"]} def process(self, instance): - data = instance.data.copy() + # data = instance.data.copy() context = instance.context self.context = context self.anatomy = instance.context.data["anatomy"] diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish.py index 15f4356b86..436ad7f195 100644 --- a/openpype/pipeline/farm/pyblish.py +++ b/openpype/pipeline/farm/pyblish.py @@ -17,10 +17,9 @@ def remap_source(source, anatomy): else: # `rootless_path` is not set to `source` if none of roots match log = Logger.get_logger("farm_publishing") - log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues." - ).format(source)) + log.warning( + ("Could not find root path for remapping \"{}\"." + " This may cause issues.").format(source)) return source From cc8732aa9d05476fb3162be176b16edacf833b0f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 7 Feb 2023 19:22:54 +0100 Subject: [PATCH 030/446] :construction: work on publish job --- openpype/modules/royalrender/api.py | 11 +- .../publish/create_maya_royalrender_job.py | 1 - .../publish/create_publish_royalrender_job.py | 102 ++++++++---------- openpype/modules/royalrender/rr_job.py | 46 +++++++- 4 files changed, 94 insertions(+), 66 deletions(-) diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index 86d27ccc6c..e610a0c8a8 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -20,19 +20,19 @@ class Api: self._rr_path = rr_path os.environ["RR_ROOT"] = rr_path - def _get_rr_bin_path(self, tool_name=None, rr_root=None): + @staticmethod + def get_rr_bin_path(rr_root, tool_name=None): # type: (str, str) -> str """Get path to RR bin folder. Args: tool_name (str): Name of RR executable you want. - rr_root (str, Optional): Custom RR root if needed. + rr_root (str): Custom RR root if needed. Returns: str: Path to the tool based on current platform. """ - rr_root = rr_root or self._rr_path is_64bit_python = sys.maxsize > 2 ** 32 rr_bin_parts = [rr_root, "bin"] @@ -65,7 +65,7 @@ class Api: # type: () -> None """Set RR modules for Python.""" # default for linux - rr_bin = self._get_rr_bin_path() + rr_bin = self.get_rr_bin_path(self._rr_path) rr_module_path = os.path.join(rr_bin, "lx64/lib") if sys.platform.lower() == "win32": @@ -109,7 +109,8 @@ class Api: def _submit_using_console(self, job_file): # type: (SubmitFile) -> None - rr_start_local = self._get_rr_bin_path("rrStartLocal") + rr_start_local = self.get_rr_bin_path( + self._rr_path, "rrStartLocal") self.log.info("rr_console: {}".format(rr_start_local)) diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 5f427353ac..0b257d8b7a 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -32,7 +32,6 @@ class CreateMayaRoyalRenderJob(InstancePlugin): Returns: RRJob: RoyalRender job payload. - """ def get_rr_platform(): if sys.platform.lower() in ["win32", "win64"]: diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 65de600bfa..f87ee589b6 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -6,6 +6,10 @@ from openpype.pipeline import legacy_io import requests import os +from openpype.modules.royalrender.rr_job import RRJob, RREnvList +from openpype.pipeline.publish import KnownPublishError +from openpype.modules.royalrender.api import Api as rrApi + class CreatePublishRoyalRenderJob(InstancePlugin): label = "Create publish job in RR" @@ -45,14 +49,12 @@ class CreatePublishRoyalRenderJob(InstancePlugin): ).format(source)) return source - def _submit_post_job(self, instance, job, instances): + def get_job(self, instance, job, instances): """Submit publish job to RoyalRender.""" data = instance.data.copy() subset = data["subset"] job_name = "Publish - {subset}".format(subset=subset) - # instance.data.get("subset") != instances[0]["subset"] - # 'Main' vs 'renderMain' override_version = None instance_version = instance.data.get("version") # take this if exists if instance_version != 1: @@ -62,6 +64,8 @@ class CreatePublishRoyalRenderJob(InstancePlugin): deepcopy(instance.data["anatomyData"]), instance.data.get("asset"), instances[0]["subset"], + # TODO: this shouldn't be hardcoded and is in fact settable by + # Settings. 'render', override_version ) @@ -71,7 +75,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): metadata_path, roothless_metadata_path = \ self._create_metadata_path(instance) - environment = { + environment = RREnvList({ "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], "AVALON_ASSET": legacy_io.Session["AVALON_ASSET"], "AVALON_TASK": legacy_io.Session["AVALON_TASK"], @@ -80,7 +84,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "OPENPYPE_RENDER_JOB": "0", "OPENPYPE_REMOTE_JOB": "0", "OPENPYPE_LOG_NO_COLORS": "1" - } + }) # add environments from self.environ_keys for env_key in self.environ_keys: @@ -88,7 +92,16 @@ class CreatePublishRoyalRenderJob(InstancePlugin): environment[env_key] = os.environ[env_key] # pass environment keys from self.environ_job_filter - job_environ = job["Props"].get("Env", {}) + # and collect all pre_ids to wait for + job_environ = {} + jobs_pre_ids = [] + for job in instance["rrJobs"]: # type: RRJob + if job.rrEnvList: + job_environ.update( + dict(RREnvList.parse(job.rrEnvList)) + ) + jobs_pre_ids.append(job.PreID) + for env_j_key in self.environ_job_filter: if job_environ.get(env_j_key): environment[env_j_key] = job_environ[env_j_key] @@ -99,7 +112,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): if mongo_url: environment["OPENPYPE_MONGO"] = mongo_url - priority = self.deadline_priority or instance.data.get("priority", 50) + priority = self.priority or instance.data.get("priority", 50) args = [ "--headless", @@ -109,66 +122,37 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "--targets", "farm" ] - # Generate the payload for Deadline submission - payload = { - "JobInfo": { - "Plugin": self.deadline_plugin, - "BatchName": job["Props"]["Batch"], - "Name": job_name, - "UserName": job["Props"]["User"], - "Comment": instance.context.data.get("comment", ""), - - "Department": self.deadline_department, - "ChunkSize": self.deadline_chunk_size, - "Priority": priority, - - "Group": self.deadline_group, - "Pool": instance.data.get("primaryPool"), - "SecondaryPool": instance.data.get("secondaryPool"), - - "OutputDirectory0": output_dir - }, - "PluginInfo": { - "Version": self.plugin_pype_version, - "Arguments": " ".join(args), - "SingleFrameOnly": "True", - }, - # Mandatory for Deadline, may be empty - "AuxFiles": [], - } + job = RRJob( + Software="Execute", + Renderer="Once", + # path to OpenPype + SeqStart=1, + SeqEnd=1, + SeqStep=1, + SeqFileOffset=0, + Version="1.0", + SceneName="", + IsActive=True, + ImageFilename="execOnce.file", + ImageDir="", + ImageExtension="", + ImagePreNumberLetter="", + SceneOS=RRJob.get_rr_platform(), + rrEnvList=environment.serialize(), + Priority=priority + ) # add assembly jobs as dependencies if instance.data.get("tileRendering"): self.log.info("Adding tile assembly jobs as dependencies...") - job_index = 0 - for assembly_id in instance.data.get("assemblySubmissionJobs"): - payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 - job_index += 1 + job.WaitForPreIDs += instance.data.get("assemblySubmissionJobs") elif instance.data.get("bakingSubmissionJobs"): self.log.info("Adding baking submission jobs as dependencies...") - job_index = 0 - for assembly_id in instance.data["bakingSubmissionJobs"]: - payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 - job_index += 1 + job.WaitForPreIDs += instance.data["bakingSubmissionJobs"] else: - payload["JobInfo"]["JobDependency0"] = job["_id"] + job.WaitForPreIDs += jobs_pre_ids - if instance.data.get("suspend_publish"): - payload["JobInfo"]["InitialStatus"] = "Suspended" - - for index, (key_, value_) in enumerate(environment.items()): - payload["JobInfo"].update( - { - "EnvironmentKeyValue%d" - % index: "{key}={value}".format( - key=key_, value=value_ - ) - } - ) - # remove secondary pool - payload["JobInfo"].pop("SecondaryPool", None) - - self.log.info("Submitting Deadline job ...") + self.log.info("Creating RoyalRender Publish job ...") url = "{}/api/jobs".format(self.deadline_url) response = requests.post(url, json=payload, timeout=10) diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index f5c7033b62..21e5291bc3 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Python wrapper for RoyalRender XML job file.""" +import sys from xml.dom import minidom as md import attr from collections import namedtuple, OrderedDict @@ -8,6 +9,23 @@ from collections import namedtuple, OrderedDict CustomAttribute = namedtuple("CustomAttribute", ["name", "value"]) +class RREnvList(dict): + def serialize(self): + # VariableA=ValueA~~~VariableB=ValueB + return "~~~".join( + ["{}={}".format(k, v) for k, v in sorted(self.items())]) + + @staticmethod + def parse(data): + # type: (str) -> RREnvList + """Parse rrEnvList string and return it as RREnvList object.""" + out = RREnvList() + for var in data.split("~~~"): + k, v = data.split("=") + out[k] = v + return out + + @attr.s class RRJob: """Mapping of Royal Render job file to a data class.""" @@ -108,7 +126,7 @@ class RRJob: # jobs send from this machine. If a job with the PreID was found, then # this jobs waits for the other job. Note: This flag can be used multiple # times to wait for multiple jobs. - WaitForPreID = attr.ib(default=None) # type: int + WaitForPreIDs = attr.ib(factory=list) # type: list # List of submitter options per job # list item must be of `SubmitterParameter` type @@ -138,6 +156,21 @@ class RRJob: TotalFrames = attr.ib(default=None) # type: int Tiled = attr.ib(default=None) # type: str + # Environment + # only used in RR 8.3 and newer + rrEnvList = attr.ib(default=None) # type: str + + @staticmethod + def get_rr_platform(): + # type: () -> str + """Returns name of platform used in rr jobs.""" + if sys.platform.lower() in ["win32", "win64"]: + return "windows" + elif sys.platform.lower() == "darwin": + return "mac" + else: + return "linux" + class SubmitterParameter: """Wrapper for Submitter Parameters.""" @@ -242,6 +275,8 @@ class SubmitFile: job, dict_factory=OrderedDict, filter=filter_data) serialized_job.pop("CustomAttributes") serialized_job.pop("SubmitterParameters") + # we are handling `WaitForPreIDs` separately. + wait_pre_ids = serialized_job.pop("WaitForPreIDs", []) for custom_attr in job_custom_attributes: # type: CustomAttribute serialized_job["Custom{}".format( @@ -253,6 +288,15 @@ class SubmitFile: root.createTextNode(str(value)) ) xml_job.appendChild(xml_attr) + + # WaitForPreID - can be used multiple times + for pre_id in wait_pre_ids: + xml_attr = root.createElement("WaitForPreID") + xml_attr.appendChild( + root.createTextNode(str(pre_id)) + ) + xml_job.appendChild(xml_attr) + job_file.appendChild(xml_job) return root.toprettyxml(indent="\t") From 5595712e810f4a6c22480f8a8fc054d426c49856 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 23 Mar 2023 16:09:25 +0100 Subject: [PATCH 031/446] :art: add cmd line arguments attribute to job --- .../plugins/publish/create_publish_royalrender_job.py | 3 +++ openpype/modules/royalrender/rr_job.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index f87ee589b6..167a97cbe4 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -131,7 +131,10 @@ class CreatePublishRoyalRenderJob(InstancePlugin): SeqStep=1, SeqFileOffset=0, Version="1.0", + # executable SceneName="", + # command line arguments + CustomAddCmdFlags=" ".join(args), IsActive=True, ImageFilename="execOnce.file", ImageDir="", diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index 21e5291bc3..689a488a5c 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -138,6 +138,9 @@ class RRJob: # list item must be of `CustomAttribute` named tuple CustomAttributes = attr.ib(factory=list) # type: list + # This is used to hold command line arguments for Execute job + CustomAddCmdFlags = attr.ib(default=None) # type: str + # Additional information for subsequent publish script and # for better display in rrControl UserName = attr.ib(default=None) # type: str From 4e14e0b56b1ad2021cd057a3cc5f939a7f334f6e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Mar 2023 15:41:13 +0100 Subject: [PATCH 032/446] :art: add render configuration --- .../render_apps/_config/E01__OpenPype.png | Bin 0 -> 2075 bytes .../_config/E01__OpenPype__PublishJob.cfg | 4 ++++ .../_config/E01__OpenPype___global.inc | 2 ++ .../render_apps/_install_paths/OpenPype.cfg | 12 ++++++++++++ .../_prepost_scripts/OpenPypeEnvironment.cfg | 11 +++++++++++ .../PreOpenPypeInjectEnvironments.py | 4 ++++ 6 files changed, 33 insertions(+) create mode 100644 openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype.png create mode 100644 openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg create mode 100644 openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype___global.inc create mode 100644 openpype/modules/royalrender/rr_root/render_apps/_install_paths/OpenPype.cfg create mode 100644 openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/OpenPypeEnvironment.cfg create mode 100644 openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/PreOpenPypeInjectEnvironments.py diff --git a/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype.png b/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype.png new file mode 100644 index 0000000000000000000000000000000000000000..68c5aec1174c9ee4b0f4ec21fe0aea8c2ac48545 GIT binary patch literal 2075 zcmah~c~BE)6pvFuK|w{7O6zo8IviqlmmpYH0z$$i1Y-yqnOY6W29_Kfb{9wxMNwL$ z9u>uds#Qlv1;wK}B9=0O6to~k1rHDhhkBrRAXXW~_S=9^ulA2k_WORn_j~Vq?|pAq zOQXZ=Y@Ka+Jf2-dxL5}M?YU>9HTeA^oZrCX4gZ;xD_Dgj3Rh8jM5(6Y3B;r~0-VS5 z4>TE-DlNf6@k9b?5WBb_y)#>ljzqF6O4)`jVwtSAWl;m zPo=Oz7zQ{rAkLDSA$0>YaD0#mltWN21VjH#Crso==p6DM-Iz$R6q8692M_R;i$VB3 zLy*1?o0ycw50NF3|EBBpZykeSLsCH^29o+#Om7@8a@WduW|&?+J%l`ya_mP~MY z!6Wp_1R{zsB(RA>*lYSbz0TcVGBNtammJ| zq>0ce#H5}uFhF-Ojv;WtM?ev!qv#mW*aR@LI2)`4Zowp!8bIFfKoKI5l%_PK4q%Kd zEEtLi5&3%g`TFt&NWTmy8xCwqjajT@0ZV`hy!n_nU*IkG^2G%{xDV!udEpplQMRzb zZBQ#&<^S=yo`(E^g+zjflMGAIX3JK8qsI`*{j2_^jf= z6PrK!UiAJJ!oO}Qudd z_Lr)XZa@SyX$#-n*n$1A$%mg}_zveWB(9KzrkPMfc} zw62ZLftGz8?5n~8(sw71$hE3`8Q{C-w#~JjZByshEfQvJnA0#Swr=Uj zi~N$3;-q83l9q3a-Fyl|BJ(6I1yL2flTIMBdim4Q{3Y|Q-|Zd;|JW5f<7g&tDcySd z)u`UGgp(Z}TV{jrg^e+M0mI`VF5ENBv@eh=P*#LENbbI2Z9|#%veL2YY5lXvOV$Ox zHylSq?h6_#m@Z4+xX$^BRaKTK#J$urXV>dbWR5Los;c0xQjBAVI|cobEB&&m;ebdV zm;LLxt**#iZJcaOOIs^b?(hF%_{@XyHB%Zpn%Z3-bb2<}OJ@zEu=Ck+nv%6P_>rK;E zRmRW7(tpU1h*pyt;Ye)y`YJnE`$3lNlM%?C8LSy?JHjT^H$;$?7~|ac7;! zUhBYR&4(5ud#!)!vx=-*TEZMCdVHa?Hb=MTYIc3S)!E8E)rIFNG5O}l+FRe3c+5Ec zY23CQLF4Y5az2-6YGXa&_Kkh5^S1Qqd>2pHf_+xaD~9c!Ybt+VdnEKsZ&mP~WYwPN(dhL@)b>RjtOnMl*vYE|tG dhq}yr)u@6>KAYxurHcEeM}$O+i-Q+u{R76+2nzrJ literal 0 HcmV?d00001 diff --git a/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg b/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg new file mode 100644 index 0000000000..3f0f8285fd --- /dev/null +++ b/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg @@ -0,0 +1,4 @@ +rendererName= PublishJob + +::include(E01__OpenPype____global.inc) +::include(E01__OpenPype____global__inhouse.inc) diff --git a/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype___global.inc b/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype___global.inc new file mode 100644 index 0000000000..ba38337340 --- /dev/null +++ b/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype___global.inc @@ -0,0 +1,2 @@ +IconApp= E01__OpenPype.png +Name= OpenPype diff --git a/openpype/modules/royalrender/rr_root/render_apps/_install_paths/OpenPype.cfg b/openpype/modules/royalrender/rr_root/render_apps/_install_paths/OpenPype.cfg new file mode 100644 index 0000000000..07f7547d29 --- /dev/null +++ b/openpype/modules/royalrender/rr_root/render_apps/_install_paths/OpenPype.cfg @@ -0,0 +1,12 @@ +[Windows] +Executable= openpype_console.exe +Path= OS; \OpenPype\*\openpype_console.exe +Path= 32; \OpenPype\*\openpype_console.exe + +[Linux] +Executable= openpype_console +Path= OS; /opt/openpype/*/openpype_console + +[Mac] +Executable= openpype_console +Path= OS; /Applications/OpenPype*/Content/MacOS/openpype_console diff --git a/openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/OpenPypeEnvironment.cfg b/openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/OpenPypeEnvironment.cfg new file mode 100644 index 0000000000..70f0bc2e24 --- /dev/null +++ b/openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/OpenPypeEnvironment.cfg @@ -0,0 +1,11 @@ +PrePostType= pre +CommandLine= + +CommandLine= rrPythonconsole" > "render_apps/_prepost_scripts/PreOpenPypeInjectEnvironments.py" + +CommandLine= + + +CommandLine= "" +CommandLine= + diff --git a/openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/PreOpenPypeInjectEnvironments.py b/openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/PreOpenPypeInjectEnvironments.py new file mode 100644 index 0000000000..891de9594c --- /dev/null +++ b/openpype/modules/royalrender/rr_root/render_apps/_prepost_scripts/PreOpenPypeInjectEnvironments.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +import os + +os.environ["OPENYPYPE_TESTVAR"] = "OpenPype was here" From f328c9848948eac8d9d8897c4bbdafb3b8981bde Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Mar 2023 15:42:06 +0100 Subject: [PATCH 033/446] :heavy_minus_sign: remove old plugin --- .../royalrender/plugins/publish/submit_maya_royalrender.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py diff --git a/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_maya_royalrender.py deleted file mode 100644 index e69de29bb2..0000000000 From de5bca48fa0f54b71783832bf16d521f6ce6a792 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Mar 2023 15:49:27 +0100 Subject: [PATCH 034/446] :art: add openpype version --- .../plugins/publish/create_maya_royalrender_job.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 0b257d8b7a..8b6beba031 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -6,10 +6,11 @@ import platform from maya.OpenMaya import MGlobal # noqa from pyblish.api import InstancePlugin, IntegratorOrder, Context +from openpype.lib import is_running_from_build from openpype.pipeline.publish.lib import get_published_workfile_instance from openpype.pipeline.publish import KnownPublishError from openpype.modules.royalrender.api import Api as rrApi -from openpype.modules.royalrender.rr_job import RRJob +from openpype.modules.royalrender.rr_job import RRJob, CustomAttribute class CreateMayaRoyalRenderJob(InstancePlugin): @@ -58,6 +59,14 @@ class CreateMayaRoyalRenderJob(InstancePlugin): layer = self._instance.data["setMembers"] # type: str layer_name = layer.removeprefix("rs_") + custom_attributes = [] + if is_running_from_build(): + custom_attributes = [ + CustomAttribute( + name="OpenPypeVersion", + value=os.environ.get("OPENPYPE_VERSION")) + ] + job = RRJob( Software="Maya", Renderer=self._instance.data["renderer"], @@ -81,7 +90,9 @@ class CreateMayaRoyalRenderJob(InstancePlugin): CompanyProjectName=self._instance.context.data["projectName"], ImageWidth=self._instance.data["resolutionWidth"], ImageHeight=self._instance.data["resolutionHeight"], + CustomAttributes=custom_attributes ) + return job def process(self, instance): From 226039f3c5fb753a935af66aefa378d797200701 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Mar 2023 18:30:49 +0100 Subject: [PATCH 035/446] :recycle: handling of jobs --- .../publish/create_maya_royalrender_job.py | 5 ++++- .../publish/create_publish_royalrender_job.py | 16 +++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 8b6beba031..6e23fb0b74 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -120,7 +120,10 @@ class CreateMayaRoyalRenderJob(InstancePlugin): self.scene_path = file_path - self._instance.data["rrJobs"] = [self.get_job()] + if not self._instance.data.get("rrJobs"): + self._instance.data["rrJobs"] = [] + + self._instance.data["rrJobs"] += self.get_job() @staticmethod def _iter_expected_files(exp): diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 167a97cbe4..03d176a324 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -123,16 +123,16 @@ class CreatePublishRoyalRenderJob(InstancePlugin): ] job = RRJob( - Software="Execute", + Software="OpenPype", Renderer="Once", # path to OpenPype SeqStart=1, SeqEnd=1, SeqStep=1, SeqFileOffset=0, - Version="1.0", + Version=os.environ.get("OPENPYPE_VERSION"), # executable - SceneName="", + SceneName=roothless_metadata_path, # command line arguments CustomAddCmdFlags=" ".join(args), IsActive=True, @@ -157,7 +157,9 @@ class CreatePublishRoyalRenderJob(InstancePlugin): self.log.info("Creating RoyalRender Publish job ...") - url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) - if not response.ok: - raise Exception(response.text) + if not instance.data.get("rrJobs"): + self.log.error("There is no RoyalRender job on the instance.") + raise KnownPublishError( + "Can't create publish job without producing jobs") + + instance.data["rrJobs"] += job From 03b3d2f37654e0b514568e99ea48e0422c82111b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Mar 2023 18:31:22 +0100 Subject: [PATCH 036/446] :art: better publish job render config --- .../_config/E01__OpenPype__PublishJob.cfg | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg b/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg index 3f0f8285fd..6414ae45a8 100644 --- a/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg +++ b/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg @@ -1,4 +1,71 @@ -rendererName= PublishJob +IconApp= E01__OpenPype.png +Name= OpenPype +rendererName= Once +Version= 1 +Version_Minor= 0 +Type=Execute +TYPEv9=Execute +ExecuteJobType=Once -::include(E01__OpenPype____global.inc) -::include(E01__OpenPype____global__inhouse.inc) + +################################# [Windows] [Linux] [Osx] ################################## + + +CommandLine=> + +CommandLine= + + +::win CommandLine= set "CUDA_VISIBLE_DEVICES=" +::lx CommandLine= setenv CUDA_VISIBLE_DEVICES +::osx CommandLine= setenv CUDA_VISIBLE_DEVICES + + +CommandLine= + + +CommandLine= + + +CommandLine= + + +CommandLine= "" --headless publish + --targets royalrender + --targets farm + + + +CommandLine= + + + + +################################## Render Settings ################################## + + + +################################## Submitter Settings ################################## +StartMultipleInstances= 0~0 +SceneFileExtension= *.json +AllowImageNameChange= 0 +AllowImageDirChange= 0 +SequenceDivide= 0~1 +PPSequenceCheck=0~0 +PPCreateSmallVideo=0~0 +PPCreateFullVideo=0~0 +AllowLocalRenderOut= 0~0 + + +################################## Client Settings ################################## + +IconApp=E01__OpenPype.png + +licenseFailLine= + +errorSearchLine= + +permanentErrorSearchLine = + +Frozen_MinCoreUsage=0.3 +Frozen_Minutes=30 From 8adb0534bf6cf36a3705b79aebc90a3894e23e63 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 29 Mar 2023 18:44:04 +0200 Subject: [PATCH 037/446] :recycle: handle OP versions better --- .../publish/create_publish_royalrender_job.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 03d176a324..9c74bdcea0 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- """Create publishing job on RoyalRender.""" -from pyblish.api import InstancePlugin, IntegratorOrder -from copy import deepcopy -from openpype.pipeline import legacy_io -import requests import os +from copy import deepcopy +from pyblish.api import InstancePlugin, IntegratorOrder + +from openpype.pipeline import legacy_io from openpype.modules.royalrender.rr_job import RRJob, RREnvList from openpype.pipeline.publish import KnownPublishError -from openpype.modules.royalrender.api import Api as rrApi +from openpype.lib.openpype_version import get_OpenPypeVersion, get_openpype_version class CreatePublishRoyalRenderJob(InstancePlugin): @@ -122,6 +122,8 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "--targets", "farm" ] + openpype_version = get_OpenPypeVersion() + current_version = openpype_version(version=get_openpype_version()) job = RRJob( Software="OpenPype", Renderer="Once", @@ -130,7 +132,8 @@ class CreatePublishRoyalRenderJob(InstancePlugin): SeqEnd=1, SeqStep=1, SeqFileOffset=0, - Version=os.environ.get("OPENPYPE_VERSION"), + Version="{}.{}".format( + current_version.major(), current_version.minor()), # executable SceneName=roothless_metadata_path, # command line arguments From 9a3fe48125d17280b7306a52005a20d1e30ed154 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Apr 2023 16:42:54 +0200 Subject: [PATCH 038/446] :art: add sop path validator --- .../publish/validate_bgeo_file_sop_path.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py diff --git a/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py b/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py new file mode 100644 index 0000000000..f4016a1b09 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +"""Validator plugin for SOP Path in bgeo isntance.""" +import pyblish.api +from openpype.pipeline import PublishValidationError + + +class ValidateNoSOPPath(pyblish.api.InstancePlugin): + """Validate if SOP Path in BGEO instance exists.""" + + order = pyblish.api.ValidatorOrder + families = ["bgeo"] + label = "Validate BGEO SOP Path" + + def process(self, instance): + + import hou + + node = hou.node(instance.data.get("instance_node")) + sop_path = node.evalParm("soppath") + if not sop_path: + raise PublishValidationError("Empty SOP Path found in " + "the bgeo isntance Geometry") + if not isinstance(hou.node(sop_path), hou.SopNode): + raise PublishValidationError( + "SOP path is not pointing to valid SOP node.") From 0d1870ff5a262c9afd28777c575971742f5b79c9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Apr 2023 16:44:19 +0200 Subject: [PATCH 039/446] :recycle: enhance sop node selection logic if selected node is sop node, it will be used for path, otherwise it will try to find output node with lowest index under selected node --- .../houdini/plugins/create/create_bgeo.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index 46fa47df92..468215d76d 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -38,13 +38,17 @@ class CreateBGEO(plugin.HoudiniCreator): } if self.selected_nodes: - parms["soppath"] = self.selected_nodes[0].path() - - # try to find output node - for child in self.selected_nodes[0].children(): - if child.type().name() == "output": - parms["soppath"] = child.path() - break + # if selection is on SOP level, use it + if isinstance(self.selected_nodes[0], hou.SopNode): + parms["soppath"] = self.selected_nodes[0].path() + else: + # try to find output node with the lowest index + outputs = [ + child for child in self.selected_nodes[0].children() + if child.type().name() == "output" + ] + outputs.sort(key=lambda output: output.evalParm("outputidx")) + parms["soppath"] = outputs[0].path() instance_node.setParms(parms) instance_node.parm("trange").set(1) From 791b04cbfade4c3f1a7c5c70db81b693c00cd4d9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Apr 2023 16:49:53 +0200 Subject: [PATCH 040/446] :memo: update docs --- website/docs/artist_hosts_houdini.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/docs/artist_hosts_houdini.md b/website/docs/artist_hosts_houdini.md index 4564e76a14..e2c1f71aa2 100644 --- a/website/docs/artist_hosts_houdini.md +++ b/website/docs/artist_hosts_houdini.md @@ -127,8 +127,7 @@ There is a simple support for publishing and loading **BGEO** files in all suppo ### Creating BGEO instances -Just select your object to be exported as BGEO. If there is `output` node inside, first found will be used as entry -point: +Just select your SOP node to be exported as BGEO. If your selection is object level, OpenPype will try to find there is `output` node inside, the one with the lowest index will be used: ![BGEO output node](assets/houdini_bgeo_output_node.png) From 547fe50ffca326937f5ed85352cda333d93a6db6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Apr 2023 17:14:36 +0200 Subject: [PATCH 041/446] :rotating_light: fix hound :dog: --- .../plugins/publish/create_publish_royalrender_job.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 9c74bdcea0..a5493dd061 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -8,7 +8,8 @@ from pyblish.api import InstancePlugin, IntegratorOrder from openpype.pipeline import legacy_io from openpype.modules.royalrender.rr_job import RRJob, RREnvList from openpype.pipeline.publish import KnownPublishError -from openpype.lib.openpype_version import get_OpenPypeVersion, get_openpype_version +from openpype.lib.openpype_version import ( + get_OpenPypeVersion, get_openpype_version) class CreatePublishRoyalRenderJob(InstancePlugin): From 23f90ec355ff4d2ce58cad7d4185ec38fcd9efc0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 17 Apr 2023 12:24:42 +0200 Subject: [PATCH 042/446] :construction: wip on skeleton instance --- openpype/pipeline/farm/pyblish.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish.py index 436ad7f195..3ad62962d2 100644 --- a/openpype/pipeline/farm/pyblish.py +++ b/openpype/pipeline/farm/pyblish.py @@ -1,5 +1,6 @@ from openpype.lib import Logger import attr +import pyblish.api @attr.s @@ -23,10 +24,27 @@ def remap_source(source, anatomy): return source -def get_skeleton_instance(): +def create_skeleton_instance(instance): + # type: (pyblish.api.Instance) -> dict + """Create skelenton instance from original instance data. + + This will create dictionary containing skeleton + - common - data used for publishing rendered instances. + This skeleton instance is then extended with additional data + and serialized to be processed by farm job. + + Args: + instance (pyblish.api.Instance): Original instance to + be used as a source of data. + + Returns: + dict: Dictionary with skeleton instance data. + """ - instance_skeleton_data = { - "family": family, + context = instance.context + + return { + "family": "render" if "prerender" not in instance.data["families"] else "prerender", # noqa: E401 "subset": subset, "families": families, "asset": asset, @@ -50,5 +68,3 @@ def get_skeleton_instance(): # map inputVersions `ObjectId` -> `str` so json supports it "inputVersions": list(map(str, data.get("inputVersions", []))) } - """ - pass From c9b6179cc84aa8347f661bfcac9d7f4fae61dcaa Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Apr 2023 02:53:06 +0200 Subject: [PATCH 043/446] :construction: move common code out from deadline --- .../plugins/publish/submit_publish_job.py | 492 +-------------- openpype/pipeline/farm/pyblish.py | 578 +++++++++++++++++- openpype/pipeline/farm/pyblish.pyi | 23 + 3 files changed, 607 insertions(+), 486 deletions(-) create mode 100644 openpype/pipeline/farm/pyblish.pyi diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 4765772bcf..fd0d87a082 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -21,35 +21,10 @@ from openpype.pipeline import ( from openpype.tests.lib import is_in_tests from openpype.pipeline.farm.patterning import match_aov_pattern from openpype.lib import is_running_from_build - - -def get_resources(project_name, version, extension=None): - """Get the files from the specific version.""" - - # TODO this functions seems to be weird - # - it's looking for representation with one extension or first (any) - # representation from a version? - # - not sure how this should work, maybe it does for specific use cases - # but probably can't be used for all resources from 2D workflows - extensions = None - if extension: - extensions = [extension] - repre_docs = list(get_representations( - project_name, version_ids=[version["_id"]], extensions=extensions - )) - assert repre_docs, "This is a bug" - - representation = repre_docs[0] - directory = get_representation_path(representation) - print("Source: ", directory) - resources = sorted( - [ - os.path.normpath(os.path.join(directory, fname)) - for fname in os.listdir(directory) - ] - ) - - return resources +from openpype.pipeline.farm.pyblish import ( + create_skeleton_instance, + create_instances_for_aov +) def get_resource_files(resources, frame_range=None): @@ -356,241 +331,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): return deadline_publish_job_id - def _copy_extend_frames(self, instance, representation): - """Copy existing frames from latest version. - - This will copy all existing frames from subset's latest version back - to render directory and rename them to what renderer is expecting. - - Arguments: - instance (pyblish.plugin.Instance): instance to get required - data from - representation (dict): presentation to operate on - - """ - import speedcopy - - self.log.info("Preparing to copy ...") - start = instance.data.get("frameStart") - end = instance.data.get("frameEnd") - project_name = legacy_io.active_project() - - # get latest version of subset - # this will stop if subset wasn't published yet - project_name = legacy_io.active_project() - version = get_last_version_by_subset_name( - project_name, - instance.data.get("subset"), - asset_name=instance.data.get("asset") - ) - - # get its files based on extension - subset_resources = get_resources( - project_name, version, representation.get("ext") - ) - r_col, _ = clique.assemble(subset_resources) - - # if override remove all frames we are expecting to be rendered - # so we'll copy only those missing from current render - if instance.data.get("overrideExistingFrame"): - for frame in range(start, end + 1): - if frame not in r_col.indexes: - continue - r_col.indexes.remove(frame) - - # now we need to translate published names from representation - # back. This is tricky, right now we'll just use same naming - # and only switch frame numbers - resource_files = [] - r_filename = os.path.basename( - representation.get("files")[0]) # first file - op = re.search(self.R_FRAME_NUMBER, r_filename) - pre = r_filename[:op.start("frame")] - post = r_filename[op.end("frame"):] - assert op is not None, "padding string wasn't found" - for frame in list(r_col): - fn = re.search(self.R_FRAME_NUMBER, frame) - # silencing linter as we need to compare to True, not to - # type - assert fn is not None, "padding string wasn't found" - # list of tuples (source, destination) - staging = representation.get("stagingDir") - staging = self.anatomy.fill_root(staging) - resource_files.append( - (frame, - os.path.join(staging, - "{}{}{}".format(pre, - fn.group("frame"), - post))) - ) - - # test if destination dir exists and create it if not - output_dir = os.path.dirname(representation.get("files")[0]) - if not os.path.isdir(output_dir): - os.makedirs(output_dir) - - # copy files - for source in resource_files: - speedcopy.copy(source[0], source[1]) - self.log.info(" > {}".format(source[1])) - - self.log.info( - "Finished copying %i files" % len(resource_files)) - - def _create_instances_for_aov( - self, instance_data, exp_files, additional_data - ): - """Create instance for each AOV found. - - This will create new instance for every aov it can detect in expected - files list. - - Arguments: - instance_data (pyblish.plugin.Instance): skeleton data for instance - (those needed) later by collector - exp_files (list): list of expected files divided by aovs - - Returns: - list of instances - - """ - task = os.environ["AVALON_TASK"] - subset = instance_data["subset"] - cameras = instance_data.get("cameras", []) - instances = [] - # go through aovs in expected files - for aov, files in exp_files[0].items(): - cols, rem = clique.assemble(files) - # we shouldn't have any reminders. And if we do, it should - # be just one item for single frame renders. - if not cols and rem: - assert len(rem) == 1, ("Found multiple non related files " - "to render, don't know what to do " - "with them.") - col = rem[0] - ext = os.path.splitext(col)[1].lstrip(".") - else: - # but we really expect only one collection. - # Nothing else make sense. - assert len(cols) == 1, "only one image sequence type is expected" # noqa: E501 - ext = cols[0].tail.lstrip(".") - col = list(cols[0]) - - self.log.debug(col) - # create subset name `familyTaskSubset_AOV` - group_name = 'render{}{}{}{}'.format( - task[0].upper(), task[1:], - subset[0].upper(), subset[1:]) - - cam = [c for c in cameras if c in col.head] - if cam: - if aov: - subset_name = '{}_{}_{}'.format(group_name, cam, aov) - else: - subset_name = '{}_{}'.format(group_name, cam) - else: - if aov: - subset_name = '{}_{}'.format(group_name, aov) - else: - subset_name = '{}'.format(group_name) - - if isinstance(col, (list, tuple)): - staging = os.path.dirname(col[0]) - else: - staging = os.path.dirname(col) - - success, rootless_staging_dir = ( - self.anatomy.find_root_template_from_path(staging) - ) - if success: - staging = rootless_staging_dir - else: - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(staging)) - - self.log.info("Creating data for: {}".format(subset_name)) - - app = os.environ.get("AVALON_APP", "") - - preview = False - - if isinstance(col, list): - render_file_name = os.path.basename(col[0]) - else: - render_file_name = os.path.basename(col) - aov_patterns = self.aov_filter - - preview = match_aov_pattern(app, aov_patterns, render_file_name) - # toggle preview on if multipart is on - - if instance_data.get("multipartExr"): - self.log.debug("Adding preview tag because its multipartExr") - preview = True - self.log.debug("preview:{}".format(preview)) - new_instance = deepcopy(instance_data) - new_instance["subset"] = subset_name - new_instance["subsetGroup"] = group_name - if preview: - new_instance["review"] = True - - # create representation - if isinstance(col, (list, tuple)): - files = [os.path.basename(f) for f in col] - else: - files = os.path.basename(col) - - # Copy render product "colorspace" data to representation. - colorspace = "" - products = additional_data["renderProducts"].layer_data.products - for product in products: - if product.productName == aov: - colorspace = product.colorspace - break - - rep = { - "name": ext, - "ext": ext, - "files": files, - "frameStart": int(instance_data.get("frameStartHandle")), - "frameEnd": int(instance_data.get("frameEndHandle")), - # If expectedFile are absolute, we need only filenames - "stagingDir": staging, - "fps": new_instance.get("fps"), - "tags": ["review"] if preview else [], - "colorspaceData": { - "colorspace": colorspace, - "config": { - "path": additional_data["colorspaceConfig"], - "template": additional_data["colorspaceTemplate"] - }, - "display": additional_data["display"], - "view": additional_data["view"] - } - } - - # support conversion from tiled to scanline - if instance_data.get("convertToScanline"): - self.log.info("Adding scanline conversion.") - rep["tags"].append("toScanline") - - # poor man exclusion - if ext in self.skip_integration_repre_list: - rep["tags"].append("delete") - - self._solve_families(new_instance, preview) - - new_instance["representations"] = [rep] - - # if extending frames from existing version, copy files from there - # into our destination directory - if new_instance.get("extendFrames", False): - self._copy_extend_frames(new_instance, rep) - instances.append(new_instance) - self.log.debug("instances:{}".format(instances)) - return instances - def _get_representations(self, instance, exp_files): """Create representations for file sequences. @@ -748,8 +488,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # type: (pyblish.api.Instance) -> None """Process plugin. - Detect type of renderfarm submission and create and post dependent job - in case of Deadline. It creates json file with metadata needed for + Detect type of render farm submission and create and post dependent + job in case of Deadline. It creates json file with metadata needed for publishing in directory of render. Args: @@ -760,145 +500,16 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Skipping local instance.") return - data = instance.data.copy() - context = instance.context - self.context = context - self.anatomy = instance.context.data["anatomy"] - - asset = data.get("asset") or legacy_io.Session["AVALON_ASSET"] - subset = data.get("subset") - - start = instance.data.get("frameStart") - if start is None: - start = context.data["frameStart"] - - end = instance.data.get("frameEnd") - if end is None: - end = context.data["frameEnd"] - - handle_start = instance.data.get("handleStart") - if handle_start is None: - handle_start = context.data["handleStart"] - - handle_end = instance.data.get("handleEnd") - if handle_end is None: - handle_end = context.data["handleEnd"] - - fps = instance.data.get("fps") - if fps is None: - fps = context.data["fps"] - - if data.get("extendFrames", False): - start, end = self._extend_frames( - asset, - subset, - start, - end, - data["overrideExistingFrame"]) - - try: - source = data["source"] - except KeyError: - source = context.data["currentFile"] - - success, rootless_path = ( - self.anatomy.find_root_template_from_path(source) - ) - if success: - source = rootless_path - - else: - # `rootless_path` is not set to `source` if none of roots match - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues." - ).format(source)) - - family = "render" - if "prerender" in instance.data["families"]: - family = "prerender" - families = [family] - - # pass review to families if marked as review - if data.get("review"): - families.append("review") - - instance_skeleton_data = { - "family": family, - "subset": subset, - "families": families, - "asset": asset, - "frameStart": start, - "frameEnd": end, - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStartHandle": start - handle_start, - "frameEndHandle": end + handle_end, - "comment": instance.data["comment"], - "fps": fps, - "source": source, - "extendFrames": data.get("extendFrames"), - "overrideExistingFrame": data.get("overrideExistingFrame"), - "pixelAspect": data.get("pixelAspect", 1), - "resolutionWidth": data.get("resolutionWidth", 1920), - "resolutionHeight": data.get("resolutionHeight", 1080), - "multipartExr": data.get("multipartExr", False), - "jobBatchName": data.get("jobBatchName", ""), - "useSequenceForReview": data.get("useSequenceForReview", True), - # map inputVersions `ObjectId` -> `str` so json supports it - "inputVersions": list(map(str, data.get("inputVersions", []))) - } - - # skip locking version if we are creating v01 - instance_version = instance.data.get("version") # take this if exists - if instance_version != 1: - instance_skeleton_data["version"] = instance_version - - # transfer specific families from original instance to new render - for item in self.families_transfer: - if item in instance.data.get("families", []): - instance_skeleton_data["families"] += [item] - - # transfer specific properties from original instance based on - # mapping dictionary `instance_transfer` - for key, values in self.instance_transfer.items(): - if key in instance.data.get("families", []): - for v in values: - instance_skeleton_data[v] = instance.data.get(v) - - # look into instance data if representations are not having any - # which are having tag `publish_on_farm` and include them - for repre in instance.data.get("representations", []): - staging_dir = repre.get("stagingDir") - if staging_dir: - success, rootless_staging_dir = ( - self.anatomy.find_root_template_from_path( - staging_dir - ) - ) - if success: - repre["stagingDir"] = rootless_staging_dir - else: - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(staging_dir)) - repre["stagingDir"] = staging_dir - - if "publish_on_farm" in repre.get("tags"): - # create representations attribute of not there - if "representations" not in instance_skeleton_data.keys(): - instance_skeleton_data["representations"] = [] - - instance_skeleton_data["representations"].append(repre) + instance_skeleton_data = create_skeleton_instance( + instance, + families_transfer=self.families_transfer, + instance_transfer=self.instance_transfer) instances = None - assert data.get("expectedFiles"), ("Submission from old Pype version" - " - missing expectedFiles") """ - if content of `expectedFiles` are dictionaries, we will handle - it as list of AOVs, creating instance from every one of them. + if content of `expectedFiles` list are dictionaries, we will handle + it as list of AOVs, creating instance for every one of them. Example: -------- @@ -920,7 +531,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): This will create instances for `beauty` and `Z` subset adding those files to their respective representations. - If we've got only list of files, we collect all filesequences. + If we have only list of files, we collect all file sequences. More then one doesn't probably make sense, but we'll handle it like creating one instance with multiple representations. @@ -938,55 +549,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): `foo` and `xxx` """ - self.log.info(data.get("expectedFiles")) - - if isinstance(data.get("expectedFiles")[0], dict): - # we cannot attach AOVs to other subsets as we consider every - # AOV subset of its own. - - additional_data = { - "renderProducts": instance.data["renderProducts"], - "colorspaceConfig": instance.data["colorspaceConfig"], - "display": instance.data["colorspaceDisplay"], - "view": instance.data["colorspaceView"] - } - - # Get templated path from absolute config path. - anatomy = instance.context.data["anatomy"] - colorspaceTemplate = instance.data["colorspaceConfig"] - success, rootless_staging_dir = ( - anatomy.find_root_template_from_path(colorspaceTemplate) - ) - if success: - colorspaceTemplate = rootless_staging_dir - else: - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(colorspaceTemplate)) - additional_data["colorspaceTemplate"] = colorspaceTemplate - - if len(data.get("attachTo")) > 0: - assert len(data.get("expectedFiles")[0].keys()) == 1, ( - "attaching multiple AOVs or renderable cameras to " - "subset is not supported") - - # create instances for every AOV we found in expected files. - # note: this is done for every AOV and every render camere (if - # there are multiple renderable cameras in scene) - instances = self._create_instances_for_aov( - instance_skeleton_data, - data.get("expectedFiles"), - additional_data - ) - self.log.info("got {} instance{}".format( - len(instances), - "s" if len(instances) > 1 else "")) + if isinstance(instance.data.get("expectedFiles")[0], dict): + instances = create_instances_for_aov( + instance, instance_skeleton_data) else: representations = self._get_representations( instance_skeleton_data, - data.get("expectedFiles") + instance.data.get("expectedFiles") ) if "representations" not in instance_skeleton_data.keys(): @@ -1029,11 +599,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): render_job = None submission_type = "" if instance.data.get("toBeRenderedOn") == "deadline": - render_job = data.pop("deadlineSubmissionJob", None) + render_job = instance.data.pop("deadlineSubmissionJob", None) submission_type = "deadline" if instance.data.get("toBeRenderedOn") == "muster": - render_job = data.pop("musterSubmissionJob", None) + render_job = instance.data.pop("musterSubmissionJob", None) submission_type = "muster" if not render_job and instance.data.get("tileRendering") is False: @@ -1056,9 +626,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "jobBatchName") else: render_job["Props"]["Batch"] = os.path.splitext( - os.path.basename(context.data.get("currentFile")))[0] + os.path.basename(instance.context.data.get("currentFile")))[0] # User is deadline user - render_job["Props"]["User"] = context.data.get( + render_job["Props"]["User"] = instance.context.data.get( "deadlineUser", getpass.getuser()) render_job["Props"]["Env"] = { @@ -1080,15 +650,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # publish job file publish_job = { - "asset": asset, - "frameStart": start, - "frameEnd": end, - "fps": context.data.get("fps", None), - "source": source, - "user": context.data["user"], - "version": context.data["version"], # this is workfile version - "intent": context.data.get("intent"), - "comment": context.data.get("comment"), + "asset": instance_skeleton_data["asset"], + "frameStart": instance_skeleton_data["frameStart"], + "frameEnd": instance_skeleton_data["frameEnd"], + "fps": instance_skeleton_data["fps"], + "source": instance_skeleton_data["source"], + "user": instance.context.data["user"], + "version": instance.context.data["version"], # this is workfile version + "intent": instance.context.data.get("intent"), + "comment": instance.context.data.get("comment"), "job": render_job or None, "session": legacy_io.Session.copy(), "instances": instances @@ -1098,7 +668,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): publish_job["deadline_publish_job_id"] = deadline_publish_job_id # add audio to metadata file if available - audio_file = context.data.get("audioFile") + audio_file = instance.context.data.get("audioFile") if audio_file and os.path.isfile(audio_file): publish_job.update({"audio": audio_file}) diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish.py index 3ad62962d2..35a944444f 100644 --- a/openpype/pipeline/farm/pyblish.py +++ b/openpype/pipeline/farm/pyblish.py @@ -1,32 +1,165 @@ +from openpype.pipeline import ( + get_current_project_name, + get_representation_path, + Anatomy, +) +from openpype.client import ( + get_last_version_by_subset_name, + get_representations +) from openpype.lib import Logger import attr import pyblish.api +from openpype.pipeline.publish import KnownPublishError +import os +import clique +from copy import deepcopy +import re @attr.s -class InstanceSkeleton(object): - # family = attr.ib(factory=) - pass +class TimeData: + """Structure used to handle time related data.""" + start = attr.ib(type=int) + end = attr.ib(type=int) + fps = attr.ib() + step = attr.ib(default=1, type=int) + handle_start = attr.ib(default=0, type=int) + handle_end = attr.ib(default=0, type=int) -def remap_source(source, anatomy): +def remap_source(path, anatomy): + """Try to remap path to rootless path. + + Args: + path (str): Path to be remapped to rootless. + anatomy (Anatomy): Anatomy object to handle remapping + itself. + + Returns: + str: Remapped path. + + Throws: + ValueError: if the root cannot be found. + + """ success, rootless_path = ( - anatomy.find_root_template_from_path(source) + anatomy.find_root_template_from_path(path) ) if success: source = rootless_path else: - # `rootless_path` is not set to `source` if none of roots match - log = Logger.get_logger("farm_publishing") - log.warning( - ("Could not find root path for remapping \"{}\"." - " This may cause issues.").format(source)) + raise ValueError( + "Root from template path cannot be found: {}".format(path)) return source -def create_skeleton_instance(instance): - # type: (pyblish.api.Instance) -> dict - """Create skelenton instance from original instance data. +def extend_frames(asset, subset, start, end): + """Get latest version of asset nad update frame range. + + Based on minimum and maximum values. + + Arguments: + asset (str): asset name + subset (str): subset name + start (int): start frame + end (int): end frame + + Returns: + (int, int): update frame start/end + + """ + # Frame comparison + prev_start = None + prev_end = None + + project_name = get_current_project_name() + version = get_last_version_by_subset_name( + project_name, + subset, + asset_name=asset + ) + + # Set prev start / end frames for comparison + if not prev_start and not prev_end: + prev_start = version["data"]["frameStart"] + prev_end = version["data"]["frameEnd"] + + updated_start = min(start, prev_start) + updated_end = max(end, prev_end) + + + return updated_start, updated_end + + +def get_time_data_from_instance_or_context(instance): + """Get time data from instance (or context). + + If time data is not found on instance, data from context will be used. + + Args: + instance (pyblish.api.Instance): Source instance. + + Returns: + TimeData: dataclass holding time information. + + """ + return TimeData( + start=instance.data.get["frameStart"] or \ + instance.context.data.get("frameStart"), + end=instance.data.get("frameEnd") or \ + instance.context.data.get("frameEnd"), + fps=instance.data.get("fps") or \ + instance.context.data.get("fps"), + handle_start=instance.data.get("handleStart") or \ + instance.context.data.get("handleStart"), # noqa: E501 + handle_end=instance.data.get("handleStart") or \ + instance.context.data.get("handleStart") + ) + + +def get_transferable_representations(instance): + """Transfer representations from original instance. + + This will get all representations on the original instance that + are flagged with `publish_on_farm` and return them to be included + on skeleton instance if needed. + + Args: + instance (pyblish.api.Instance): Original instance to be processed. + + Return: + list of dicts: List of transferable representations. + + """ + anatomy = instance.data.get("anatomy") # type: Anatomy + to_transfer = [] + + for representation in instance.data.get("representations", []): + if "publish_on_farm" not in representation.get("tags"): + continue + + trans_rep = representation.copy() + + staging_dir = trans_rep.get("stagingDir") + + if staging_dir: + try: + trans_rep["stagingDir"] = remap_source(staging_dir, anatomy) + except ValueError: + log = Logger.get_logger("farm_publishing") + log.warning( + ("Could not find root path for remapping \"{}\". " + "This may cause issues on farm.").format(staging_dir)) + + to_transfer.append(trans_rep) + return to_transfer + + +def create_skeleton_instance( + instance, families_transfer=None, instance_transfer=None): + # type: (pyblish.api.Instance, list, dict) -> dict + """Create skeleton instance from original instance data. This will create dictionary containing skeleton - common - data used for publishing rendered instances. @@ -36,26 +169,64 @@ def create_skeleton_instance(instance): Args: instance (pyblish.api.Instance): Original instance to be used as a source of data. + families_transfer (list): List of family names to transfer + from the original instance to the skeleton. + instance_transfer (dict): Dict with keys as families and + values as a list of property names to transfer to the + new skeleton. Returns: dict: Dictionary with skeleton instance data. """ - context = instance.context + # list of family names to transfer to new family if present - return { + context = instance.context + data = instance.data.copy() + anatomy = data["anatomy"] # type: Anatomy + + families = [data["family"]] + + # pass review to families if marked as review + if data.get("review"): + families.append("review") + + # get time related data from instance (or context) + time_data = get_time_data_from_instance_or_context(instance) + + if data.get("extendFrames", False): + time_data.start, time_data.end = extend_frames( + data["asset"], + data["subset"], + time_data.start, + time_data.end, + ) + + source = data.get("source") or context.data.get("currentFile") + success, rootless_path = ( + anatomy.find_root_template_from_path(source) + ) + if success: + source = rootless_path + else: + # `rootless_path` is not set to `source` if none of roots match + log = Logger.get_logger("farm_publishing") + log.warning(("Could not find root path for remapping \"{}\". " + "This may cause issues.").format(source)) + + instance_skeleton_data = { "family": "render" if "prerender" not in instance.data["families"] else "prerender", # noqa: E401 - "subset": subset, + "subset": data["subset"], "families": families, - "asset": asset, - "frameStart": start, - "frameEnd": end, - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStartHandle": start - handle_start, - "frameEndHandle": end + handle_end, - "comment": instance.data["comment"], - "fps": fps, + "asset": data["asset"], + "frameStart": time_data.start, + "frameEnd": time_data.end, + "handleStart": time_data.handle_start, + "handleEnd": time_data.handle_end, + "frameStartHandle": time_data.start - time_data.handle_start, + "frameEndHandle": time_data.end + time_data.handle_end, + "comment": data.get("comment"), + "fps": time_data.fps, "source": source, "extendFrames": data.get("extendFrames"), "overrideExistingFrame": data.get("overrideExistingFrame"), @@ -68,3 +239,360 @@ def create_skeleton_instance(instance): # map inputVersions `ObjectId` -> `str` so json supports it "inputVersions": list(map(str, data.get("inputVersions", []))) } + + # skip locking version if we are creating v01 + instance_version = data.get("version") # take this if exists + if instance_version != 1: + instance_skeleton_data["version"] = instance_version + + # transfer specific families from original instance to new render + for item in families_transfer: + if item in instance.data.get("families", []): + instance_skeleton_data["families"] += [item] + + # transfer specific properties from original instance based on + # mapping dictionary `instance_transfer` + for key, values in instance_transfer.items(): + if key in instance.data.get("families", []): + for v in values: + instance_skeleton_data[v] = instance.data.get(v) + + representations = get_transferable_representations(instance) + instance_skeleton_data["representations"] = [] + instance_skeleton_data["representations"] += representations + + return instance_skeleton_data + +def _solve_families(families): + """Solve families. + + TODO: This is ugly and needs to be refactored. Ftrack family should be + added in different way (based on if the module is enabled?) + + """ + # if we have one representation with preview tag + # flag whole instance for review and for ftrack + if "ftrack" not in families and os.environ.get("FTRACK_SERVER"): + families.append("ftrack") + if "review" not in families: + families.append("review") + return families +def create_instances_for_aov(instance, skeleton): + """Create instances from AOVs. + + This will create new pyblish.api.Instances by going over expected + files defined on original instance. + + Args: + instance (pyblish.api.Instance): Original instance. + skeleton (dict): Skeleton instance data. + + Returns: + list of pyblish.api.Instance: Instances created from + expected files. + + """ + # we cannot attach AOVs to other subsets as we consider every + # AOV subset of its own. + + log = Logger.get_logger("farm_publishing") + additional_color_data = { + "renderProducts": instance.data["renderProducts"], + "colorspaceConfig": instance.data["colorspaceConfig"], + "display": instance.data["colorspaceDisplay"], + "view": instance.data["colorspaceView"] + } + + # Get templated path from absolute config path. + anatomy = instance.context.data["anatomy"] + colorspace_template = instance.data["colorspaceConfig"] + try: + additional_color_data["colorspaceTemplate"] = remap_source( + colorspace_template, anatomy) + except ValueError as e: + log.warning(e) + + # if there are subset to attach to and more than one AOV, + # we cannot proceed. + if ( + len(instance.data.get("attachTo", [])) > 0 + and len(instance.data.get("expectedFiles")[0].keys()) != 1 + ): + raise KnownPublishError( + "attaching multiple AOVs or renderable cameras to " + "subset is not supported yet.") + + # create instances for every AOV we found in expected files. + # NOTE: this is done for every AOV and every render camera (if + # there are multiple renderable cameras in scene) + return _create_instances_for_aov( + instance, + skeleton, + additional_color_data + ) + + +def _create_instances_for_aov(instance, skeleton, additional_data): + """Create instance for each AOV found. + + This will create new instance for every AOV it can detect in expected + files list. + + Args: + instance (pyblish.api.Instance): Original instance. + skeleton (dict): Skeleton data for instance (those needed) later + by collector. + additional_data (dict): ... + + + Returns: + list of instances + + Throws: + ValueError: + + """ + # TODO: this needs to be taking the task from context or instance + task = os.environ["AVALON_TASK"] + + anatomy = instance.data["anatomy"] + subset = skeleton["subset"] + cameras = instance.data.get("cameras", []) + exp_files = instance.data["expectedFiles"] + log = Logger.get_logger("farm_publishing") + + instances = [] + # go through AOVs in expected files + for aov, files in exp_files[0].items(): + cols, rem = clique.assemble(files) + # we shouldn't have any reminders. And if we do, it should + # be just one item for single frame renders. + if not cols and rem: + if len(rem) != 1: + raise ValueError("Found multiple non related files " + "to render, don't know what to do " + "with them.") + col = rem[0] + ext = os.path.splitext(col)[1].lstrip(".") + else: + # but we really expect only one collection. + # Nothing else make sense. + if len(cols) != 1: + raise ValueError("Only one image sequence type is expected.") # noqa: E501 + ext = cols[0].tail.lstrip(".") + col = list(cols[0]) + + # create subset name `familyTaskSubset_AOV` + group_name = 'render{}{}{}{}'.format( + task[0].upper(), task[1:], + subset[0].upper(), subset[1:]) + + cam = [c for c in cameras if c in col.head] + if cam: + if aov: + subset_name = '{}_{}_{}'.format(group_name, cam, aov) + else: + subset_name = '{}_{}'.format(group_name, cam) + else: + if aov: + subset_name = '{}_{}'.format(group_name, aov) + else: + subset_name = '{}'.format(group_name) + + if isinstance(col, (list, tuple)): + staging = os.path.dirname(col[0]) + else: + staging = os.path.dirname(col) + + try: + staging = remap_source(staging, anatomy) + except ValueError as e: + log.warning(e) + + log.info("Creating data for: {}".format(subset_name)) + + app = os.environ.get("AVALON_APP", "") + + preview = False + + if isinstance(col, list): + render_file_name = os.path.basename(col[0]) + else: + render_file_name = os.path.basename(col) + aov_patterns = self.aov_filter + + preview = match_aov_pattern(app, aov_patterns, render_file_name) + # toggle preview on if multipart is on + + if instance.data.get("multipartExr"): + log.debug("Adding preview tag because its multipartExr") + preview = True + + + new_instance = deepcopy(skeleton) + new_instance["subsetGroup"] = group_name + if preview: + new_instance["review"] = True + + # create representation + if isinstance(col, (list, tuple)): + files = [os.path.basename(f) for f in col] + else: + files = os.path.basename(col) + + # Copy render product "colorspace" data to representation. + colorspace = "" + products = additional_data["renderProducts"].layer_data.products + for product in products: + if product.productName == aov: + colorspace = product.colorspace + break + + rep = { + "name": ext, + "ext": ext, + "files": files, + "frameStart": int(skeleton["frameStartHandle"]), + "frameEnd": int(skeleton["frameEndHandle"]), + # If expectedFile are absolute, we need only filenames + "stagingDir": staging, + "fps": new_instance.get("fps"), + "tags": ["review"] if preview else [], + "colorspaceData": { + "colorspace": colorspace, + "config": { + "path": additional_data["colorspaceConfig"], + "template": additional_data["colorspaceTemplate"] + }, + "display": additional_data["display"], + "view": additional_data["view"] + } + } + + # support conversion from tiled to scanline + if instance.data.get("convertToScanline"): + log.info("Adding scanline conversion.") + rep["tags"].append("toScanline") + + # poor man exclusion + if ext in self.skip_integration_repre_list: + rep["tags"].append("delete") + + if preview: + new_instance["families"] = _solve_families(new_instance) + + new_instance["representations"] = [rep] + + # if extending frames from existing version, copy files from there + # into our destination directory + if new_instance.get("extendFrames", False): + copy_extend_frames(new_instance, rep) + instances.append(new_instance) + log.debug("instances:{}".format(instances)) + return instances + +def get_resources(project_name, version, extension=None): + """Get the files from the specific version.""" + + # TODO this functions seems to be weird + # - it's looking for representation with one extension or first (any) + # representation from a version? + # - not sure how this should work, maybe it does for specific use cases + # but probably can't be used for all resources from 2D workflows + extensions = None + if extension: + extensions = [extension] + repre_docs = list(get_representations( + project_name, version_ids=[version["_id"]], extensions=extensions + )) + assert repre_docs, "This is a bug" + + representation = repre_docs[0] + directory = get_representation_path(representation) + print("Source: ", directory) + resources = sorted( + [ + os.path.normpath(os.path.join(directory, fname)) + for fname in os.listdir(directory) + ] + ) + + return resources + +def copy_extend_frames(self, instance, representation): + """Copy existing frames from latest version. + + This will copy all existing frames from subset's latest version back + to render directory and rename them to what renderer is expecting. + + Arguments: + instance (pyblish.plugin.Instance): instance to get required + data from + representation (dict): presentation to operate on + + """ + import speedcopy + + log = Logger.get_logger("farm_publishing") + log.info("Preparing to copy ...") + start = instance.data.get("frameStart") + end = instance.data.get("frameEnd") + project_name = instance.context.data["project"] + + # get latest version of subset + # this will stop if subset wasn't published yet + + version = get_last_version_by_subset_name( + project_name, + instance.data.get("subset"), + asset_name=instance.data.get("asset") + ) + + # get its files based on extension + subset_resources = get_resources( + project_name, version, representation.get("ext") + ) + r_col, _ = clique.assemble(subset_resources) + + # if override remove all frames we are expecting to be rendered + # so we'll copy only those missing from current render + if instance.data.get("overrideExistingFrame"): + for frame in range(start, end + 1): + if frame not in r_col.indexes: + continue + r_col.indexes.remove(frame) + + # now we need to translate published names from representation + # back. This is tricky, right now we'll just use same naming + # and only switch frame numbers + resource_files = [] + r_filename = os.path.basename( + representation.get("files")[0]) # first file + op = re.search(self.R_FRAME_NUMBER, r_filename) + pre = r_filename[:op.start("frame")] + post = r_filename[op.end("frame"):] + assert op is not None, "padding string wasn't found" + for frame in list(r_col): + fn = re.search(self.R_FRAME_NUMBER, frame) + # silencing linter as we need to compare to True, not to + # type + assert fn is not None, "padding string wasn't found" + # list of tuples (source, destination) + staging = representation.get("stagingDir") + staging = self.anatomy.fill_root(staging) + resource_files.append( + (frame, os.path.join( + staging, "{}{}{}".format(pre, fn["frame"], post))) + ) + + # test if destination dir exists and create it if not + output_dir = os.path.dirname(representation.get("files")[0]) + if not os.path.isdir(output_dir): + os.makedirs(output_dir) + + # copy files + for source in resource_files: + speedcopy.copy(source[0], source[1]) + log.info(" > {}".format(source[1])) + + log.info("Finished copying %i files" % len(resource_files)) diff --git a/openpype/pipeline/farm/pyblish.pyi b/openpype/pipeline/farm/pyblish.pyi new file mode 100644 index 0000000000..3667f2d8a5 --- /dev/null +++ b/openpype/pipeline/farm/pyblish.pyi @@ -0,0 +1,23 @@ +import pyblish.api +from openpype.pipeline import Anatomy +from typing import Tuple, Union, List + + +class TimeData: + start: int + end: int + fps: float | int + step: int + handle_start: int + handle_end: int + + def __init__(self, start: int, end: int, fps: float | int, step: int, handle_start: int, handle_end: int): + ... + ... + +def remap_source(source: str, anatomy: Anatomy): ... +def extend_frames(asset: str, subset: str, start: int, end: int) -> Tuple[int, int]: ... +def get_time_data_from_instance_or_context(instance: pyblish.api.Instance) -> TimeData: ... +def get_transferable_representations(instance: pyblish.api.Instance) -> list: ... +def create_skeleton_instance(instance: pyblish.api.Instance, families_transfer: list = ..., instance_transfer: dict = ...) -> dict: ... +def create_instances_for_aov(instance: pyblish.api.Instance, skeleton: dict) -> List[pyblish.api.Instance]: ... From 7f588ee35674c364100acaa7e5b80a4364ed697a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Apr 2023 02:54:38 +0200 Subject: [PATCH 044/446] :heavy_plus_sign: add mypy --- poetry.lock | 68 +++++++++++++++++++++++++++++++++++++++++++++++--- pyproject.toml | 1 + 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index f71611cb6f..d5b80d0f0a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. [[package]] name = "acre" @@ -1456,11 +1456,13 @@ python-versions = ">=3.6" files = [ {file = "lief-0.12.3-cp310-cp310-macosx_10_14_arm64.whl", hash = "sha256:66724f337e6a36cea1a9380f13b59923f276c49ca837becae2e7be93a2e245d9"}, {file = "lief-0.12.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6d18aafa2028587c98f6d4387bec94346e92f2b5a8a5002f70b1cf35b1c045cc"}, + {file = "lief-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f69d125caaa8d5ddb574f29cc83101e165ebea1a9f18ad042eb3544081a797"}, {file = "lief-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c078d6230279ffd3bca717c79664fb8368666f610b577deb24b374607936e9c1"}, {file = "lief-0.12.3-cp310-cp310-win32.whl", hash = "sha256:e3a6af926532d0aac9e7501946134513d63217bacba666e6f7f5a0b7e15ba236"}, {file = "lief-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:0750b72e3aa161e1fb0e2e7f571121ae05d2428aafd742ff05a7656ad2288447"}, {file = "lief-0.12.3-cp311-cp311-macosx_10_14_arm64.whl", hash = "sha256:b5c123cb99a7879d754c059e299198b34e7e30e3b64cf22e8962013db0099f47"}, {file = "lief-0.12.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8bc58fa26a830df6178e36f112cb2bbdd65deff593f066d2d51434ff78386ba5"}, + {file = "lief-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74ac6143ac6ccd813c9b068d9c5f1f9d55c8813c8b407387eb57de01c3db2d74"}, {file = "lief-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04eb6b70d646fb5bd6183575928ee23715550f161f2832cbcd8c6ff2071fb408"}, {file = "lief-0.12.3-cp311-cp311-win32.whl", hash = "sha256:7e2d0a53c403769b04adcf8df92e83c5e25f9103a052aa7f17b0a9cf057735fb"}, {file = "lief-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:7f6395c12ee1bc4a5162f567cba96d0c72dfb660e7902e84d4f3029daf14fe33"}, @@ -1480,6 +1482,7 @@ files = [ {file = "lief-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:b00667257b43e93d94166c959055b6147d46d302598f3ee55c194b40414c89cc"}, {file = "lief-0.12.3-cp39-cp39-macosx_10_14_arm64.whl", hash = "sha256:e6a1b5b389090d524621c2455795e1262f62dc9381bedd96f0cd72b878c4066d"}, {file = "lief-0.12.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ae773196df814202c0c51056163a1478941b299512b09660a3c37be3c7fac81e"}, + {file = "lief-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:66ddf88917ec7b00752687c476bb2771dc8ec19bd7e4c0dcff1f8ef774cad4e9"}, {file = "lief-0.12.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4a47f410032c63ac3be051d963d0337d6b47f0e94bfe8e946ab4b6c428f4d0f8"}, {file = "lief-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbd11367c2259bd1131a6c8755dcde33314324de5ea029227bfbc7d3755871e6"}, {file = "lief-0.12.3-cp39-cp39-win32.whl", hash = "sha256:2ce53e311918c3e5b54c815ef420a747208d2a88200c41cd476f3dd1eb876bcf"}, @@ -1676,6 +1679,65 @@ files = [ {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, ] +[[package]] +name = "mypy" +version = "1.2.0" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:701189408b460a2ff42b984e6bd45c3f41f0ac9f5f58b8873bbedc511900086d"}, + {file = "mypy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe91be1c51c90e2afe6827601ca14353bbf3953f343c2129fa1e247d55fd95ba"}, + {file = "mypy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d26b513225ffd3eacece727f4387bdce6469192ef029ca9dd469940158bc89e"}, + {file = "mypy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3a2d219775a120581a0ae8ca392b31f238d452729adbcb6892fa89688cb8306a"}, + {file = "mypy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2e93a8a553e0394b26c4ca683923b85a69f7ccdc0139e6acd1354cc884fe0128"}, + {file = "mypy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3efde4af6f2d3ccf58ae825495dbb8d74abd6d176ee686ce2ab19bd025273f41"}, + {file = "mypy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:695c45cea7e8abb6f088a34a6034b1d273122e5530aeebb9c09626cea6dca4cb"}, + {file = "mypy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0e9464a0af6715852267bf29c9553e4555b61f5904a4fc538547a4d67617937"}, + {file = "mypy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8293a216e902ac12779eb7a08f2bc39ec6c878d7c6025aa59464e0c4c16f7eb9"}, + {file = "mypy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f46af8d162f3d470d8ffc997aaf7a269996d205f9d746124a179d3abe05ac602"}, + {file = "mypy-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:031fc69c9a7e12bcc5660b74122ed84b3f1c505e762cc4296884096c6d8ee140"}, + {file = "mypy-1.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:390bc685ec209ada4e9d35068ac6988c60160b2b703072d2850457b62499e336"}, + {file = "mypy-1.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4b41412df69ec06ab141808d12e0bf2823717b1c363bd77b4c0820feaa37249e"}, + {file = "mypy-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e4a682b3f2489d218751981639cffc4e281d548f9d517addfd5a2917ac78119"}, + {file = "mypy-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a197ad3a774f8e74f21e428f0de7f60ad26a8d23437b69638aac2764d1e06a6a"}, + {file = "mypy-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9a084bce1061e55cdc0493a2ad890375af359c766b8ac311ac8120d3a472950"}, + {file = "mypy-1.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaeaa0888b7f3ccb7bcd40b50497ca30923dba14f385bde4af78fac713d6d6f6"}, + {file = "mypy-1.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bea55fc25b96c53affab852ad94bf111a3083bc1d8b0c76a61dd101d8a388cf5"}, + {file = "mypy-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:4c8d8c6b80aa4a1689f2a179d31d86ae1367ea4a12855cc13aa3ba24bb36b2d8"}, + {file = "mypy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70894c5345bea98321a2fe84df35f43ee7bb0feec117a71420c60459fc3e1eed"}, + {file = "mypy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4a99fe1768925e4a139aace8f3fb66db3576ee1c30b9c0f70f744ead7e329c9f"}, + {file = "mypy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023fe9e618182ca6317ae89833ba422c411469156b690fde6a315ad10695a521"}, + {file = "mypy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d19f1a239d59f10fdc31263d48b7937c585810288376671eaf75380b074f238"}, + {file = "mypy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:2de7babe398cb7a85ac7f1fd5c42f396c215ab3eff731b4d761d68d0f6a80f48"}, + {file = "mypy-1.2.0-py3-none-any.whl", hash = "sha256:d8e9187bfcd5ffedbe87403195e1fc340189a68463903c39e2b63307c9fa0394"}, + {file = "mypy-1.2.0.tar.gz", hash = "sha256:f70a40410d774ae23fcb4afbbeca652905a04de7948eaf0b1789c8d1426b72d1"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "nodeenv" version = "1.7.0" @@ -2352,7 +2414,7 @@ files = [ cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] @@ -3462,4 +3524,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "02daca205796a0f29a0d9f50707544e6804f32027eba493cd2aa7f175a00dcea" +content-hash = "9d3a574b1b6f42ae05d4f0fa6d65677ee54a51c53d984dd3f44d02f234962dbb" diff --git a/pyproject.toml b/pyproject.toml index 42ce5aa32c..afc6cc45d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,7 @@ wheel = "*" enlighten = "*" # cool terminal progress bars toml = "^0.10.2" # for parsing pyproject.toml pre-commit = "*" +mypy = "*" # for better types [tool.poetry.urls] "Bug Tracker" = "https://github.com/pypeclub/openpype/issues" From 3a9109422d499f9ab843e1fa26f8a02c2d463f55 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 28 Apr 2023 15:17:47 +0200 Subject: [PATCH 045/446] :art: update code from upstream --- openpype/pipeline/farm/tools.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/openpype/pipeline/farm/tools.py b/openpype/pipeline/farm/tools.py index 8cf1af399e..506f95d6b2 100644 --- a/openpype/pipeline/farm/tools.py +++ b/openpype/pipeline/farm/tools.py @@ -53,14 +53,11 @@ def from_published_scene(instance, replace_in_path=True): template_data["comment"] = None anatomy = instance.context.data['anatomy'] - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] + template_obj = anatomy.templates_obj["publish"]["path"] + template_filled = template_obj.format_strict(template_data) file_path = os.path.normpath(template_filled) - self.log.info("Using published scene for render {}".format(file_path)) - if not os.path.exists(file_path): - self.log.error("published scene does not exist!") raise if not replace_in_path: @@ -102,8 +99,4 @@ def from_published_scene(instance, replace_in_path=True): new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) - return file_path From 92bb47ed231216ea51f4cdb70084358190082d12 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 28 Apr 2023 18:37:26 +0200 Subject: [PATCH 046/446] :art: add nuke job creator --- .../publish/create_nuke_deadline_job.py | 307 ++++++++++++++++++ .../publish/create_publish_royalrender_job.py | 117 ++++--- openpype/modules/royalrender/rr_job.py | 22 +- openpype/pipeline/farm/pyblish.py | 114 +++++-- openpype/pipeline/farm/pyblish.pyi | 3 +- 5 files changed, 487 insertions(+), 76 deletions(-) create mode 100644 openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py new file mode 100644 index 0000000000..9f49294459 --- /dev/null +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +"""Submitting render job to RoyalRender.""" +import os +import sys +import re +import platform +from datetime import datetime + +from pyblish.api import InstancePlugin, IntegratorOrder, Context +from openpype.tests.lib import is_in_tests +from openpype.lib import is_running_from_build +from openpype.pipeline.publish.lib import get_published_workfile_instance +from openpype.pipeline.publish import KnownPublishError +from openpype.modules.royalrender.api import Api as rrApi +from openpype.modules.royalrender.rr_job import ( + RRJob, CustomAttribute, get_rr_platform) +from openpype.lib import ( + is_running_from_build, + BoolDef, + NumberDef +) + + +class CreateNukeRoyalRenderJob(InstancePlugin): + label = "Create Nuke Render job in RR" + order = IntegratorOrder + 0.1 + hosts = ["nuke"] + families = ["render", "prerender"] + targets = ["local"] + optional = True + + priority = 50 + chunk_size = 1 + concurrent_tasks = 1 + + @classmethod + def get_attribute_defs(cls): + return [ + NumberDef( + "priority", + label="Priority", + default=cls.priority, + decimals=0 + ), + NumberDef( + "chunk", + label="Frames Per Task", + default=cls.chunk_size, + decimals=0, + minimum=1, + maximum=1000 + ), + NumberDef( + "concurrency", + label="Concurrency", + default=cls.concurrent_tasks, + decimals=0, + minimum=1, + maximum=10 + ), + BoolDef( + "use_gpu", + default=cls.use_gpu, + label="Use GPU" + ), + BoolDef( + "suspend_publish", + default=False, + label="Suspend publish" + ) + ] + + def __init__(self, *args, **kwargs): + self._instance = None + self._rr_root = None + self.scene_path = None + self.job = None + self.submission_parameters = None + self.rr_api = None + + def process(self, instance): + if not instance.data.get("farm"): + self.log.info("Skipping local instance.") + return + + instance.data["attributeValues"] = self.get_attr_values_from_data( + instance.data) + + # add suspend_publish attributeValue to instance data + instance.data["suspend_publish"] = instance.data["attributeValues"][ + "suspend_publish"] + + context = instance.context + + self._rr_root = self._resolve_rr_path(context, instance.data.get( + "rrPathName")) # noqa + self.log.debug(self._rr_root) + if not self._rr_root: + raise KnownPublishError( + ("Missing RoyalRender root. " + "You need to configure RoyalRender module.")) + + self.rr_api = rrApi(self._rr_root) + + self.scene_path = context.data["currentFile"] + if self.use_published: + file_path = get_published_workfile_instance(context) + + # fallback if nothing was set + if not file_path: + self.log.warning("Falling back to workfile") + file_path = context.data["currentFile"] + + self.scene_path = file_path + self.log.info( + "Using published scene for render {}".format(self.scene_path) + ) + + if not self._instance.data.get("expectedFiles"): + self._instance.data["expectedFiles"] = [] + + if not self._instance.data.get("rrJobs"): + self._instance.data["rrJobs"] = [] + + self._instance.data["rrJobs"] += self.create_jobs() + + # redefinition of families + if "render" in self._instance.data["family"]: + self._instance.data["family"] = "write" + self._instance.data["families"].insert(0, "render2d") + elif "prerender" in self._instance.data["family"]: + self._instance.data["family"] = "write" + self._instance.data["families"].insert(0, "prerender") + + self._instance.data["outputDir"] = os.path.dirname( + self._instance.data["path"]).replace("\\", "/") + + + def create_jobs(self): + submit_frame_start = int(self._instance.data["frameStartHandle"]) + submit_frame_end = int(self._instance.data["frameEndHandle"]) + + # get output path + render_path = self._instance.data['path'] + script_path = self.scene_path + node = self._instance.data["transientData"]["node"] + + # main job + jobs = [ + self.get_job( + script_path, + render_path, + node.name(), + submit_frame_start, + submit_frame_end, + ) + ] + + for baking_script in self._instance.data.get("bakingNukeScripts", []): + render_path = baking_script["bakeRenderPath"] + script_path = baking_script["bakeScriptPath"] + exe_node_name = baking_script["bakeWriteNodeName"] + + jobs.append(self.get_job( + script_path, + render_path, + exe_node_name, + submit_frame_start, + submit_frame_end + )) + + return jobs + + def get_job(self, script_path, render_path, + node_name, start_frame, end_frame): + """Get RR job based on current instance. + + Args: + script_path (str): Path to Nuke script. + render_path (str): Output path. + node_name (str): Name of the render node. + start_frame (int): Start frame. + end_frame (int): End frame. + + Returns: + RRJob: RoyalRender Job instance. + + """ + render_dir = os.path.normpath(os.path.dirname(render_path)) + batch_name = os.path.basename(script_path) + jobname = "%s - %s" % (batch_name, self._instance.name) + if is_in_tests(): + batch_name += datetime.now().strftime("%d%m%Y%H%M%S") + + output_filename_0 = self.preview_fname(render_path) + + custom_attributes = [] + if is_running_from_build(): + custom_attributes = [ + CustomAttribute( + name="OpenPypeVersion", + value=os.environ.get("OPENPYPE_VERSION")) + ] + + nuke_version = re.search( + r"\d+\.\d+", self._instance.context.data.get("hostVersion")) + + # this will append expected files to instance as needed. + expected_files = self.expected_files( + render_path, start_frame, end_frame) + first_file = next(self._iter_expected_files(expected_files)) + + job = RRJob( + Software="Nuke", + Renderer="", + SeqStart=int(start_frame), + SeqEnd=int(end_frame), + SeqStep=int(self._instance.data.get("byFrameStep"), 1), + SeqFileOffset=0, + Version=nuke_version.group(), + SceneName=script_path, + IsActive=True, + ImageDir=render_dir.replace("\\", "/"), + ImageFilename="{}.".format(os.path.splitext(first_file)[0]), + ImageExtension=os.path.splitext(first_file)[1], + ImagePreNumberLetter=".", + ImageSingleOutputFile=False, + SceneOS=get_rr_platform(), + Layer=node_name, + SceneDatabaseDir=script_path, + CustomSHotName=self._instance.context.data["asset"], + CompanyProjectName=self._instance.context.data["projectName"], + ImageWidth=self._instance.data["resolutionWidth"], + ImageHeight=self._instance.data["resolutionHeight"], + CustomAttributes=custom_attributes + ) + + @staticmethod + def _resolve_rr_path(context, rr_path_name): + # type: (Context, str) -> str + rr_settings = ( + context.data + ["system_settings"] + ["modules"] + ["royalrender"] + ) + try: + default_servers = rr_settings["rr_paths"] + project_servers = ( + context.data + ["project_settings"] + ["royalrender"] + ["rr_paths"] + ) + rr_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + + except (AttributeError, KeyError): + # Handle situation were we had only one url for royal render. + return context.data["defaultRRPath"][platform.system().lower()] + + return rr_servers[rr_path_name][platform.system().lower()] + + def expected_files(self, path, start_frame, end_frame): + """Get expected files. + + This function generate expected files from provided + path and start/end frames. + + It was taken from Deadline module, but this should be + probably handled better in collector to support more + flexible scenarios. + + Args: + path (str): Output path. + start_frame (int): Start frame. + end_frame (int): End frame. + + Returns: + list: List of expected files. + + """ + dir_name = os.path.dirname(path) + file = os.path.basename(path) + + expected_files = [] + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + expected_files.append(path) + return + + if self._instance.data.get("slate"): + start_frame -= 1 + + expected_files.extend( + os.path.join(dir_name, (file % i)).replace("\\", "/") + for i in range(start_frame, (end_frame + 1)) + ) + return expected_files diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index a5493dd061..b1c84c87b9 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -2,14 +2,20 @@ """Create publishing job on RoyalRender.""" import os from copy import deepcopy +import json -from pyblish.api import InstancePlugin, IntegratorOrder +from pyblish.api import InstancePlugin, IntegratorOrder, Instance from openpype.pipeline import legacy_io from openpype.modules.royalrender.rr_job import RRJob, RREnvList from openpype.pipeline.publish import KnownPublishError from openpype.lib.openpype_version import ( get_OpenPypeVersion, get_openpype_version) +from openpype.pipeline.farm.pyblish import ( + create_skeleton_instance, + create_instances_for_aov, + attach_instances_to_subset +) class CreatePublishRoyalRenderJob(InstancePlugin): @@ -31,50 +37,84 @@ class CreatePublishRoyalRenderJob(InstancePlugin): self.context = context self.anatomy = instance.context.data["anatomy"] - # asset = data.get("asset") - # subset = data.get("subset") - # source = self._remap_source( - # data.get("source") or context.data["source"]) + if not instance.data.get("farm"): + self.log.info("Skipping local instance.") + return + + instance_skeleton_data = create_skeleton_instance( + instance, + families_transfer=self.families_transfer, + instance_transfer=self.instance_transfer) + + instances = None + if isinstance(instance.data.get("expectedFiles")[0], dict): + instances = create_instances_for_aov( + instance, instance_skeleton_data, self.aov_filter) - def _remap_source(self, source): - success, rootless_path = ( - self.anatomy.find_root_template_from_path(source) - ) - if success: - source = rootless_path else: - # `rootless_path` is not set to `source` if none of roots match - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues." - ).format(source)) - return source + representations = self._get_representations( + instance_skeleton_data, + instance.data.get("expectedFiles") + ) - def get_job(self, instance, job, instances): - """Submit publish job to RoyalRender.""" + if "representations" not in instance_skeleton_data.keys(): + instance_skeleton_data["representations"] = [] + + # add representation + instance_skeleton_data["representations"] += representations + instances = [instance_skeleton_data] + + # attach instances to subset + if instance.data.get("attachTo"): + instances = attach_instances_to_subset( + instance.data.get("attachTo"), instances + ) + + self.log.info("Creating RoyalRender Publish job ...") + + if not instance.data.get("rrJobs"): + self.log.error(("There is no prior RoyalRender " + "job on the instance.")) + raise KnownPublishError( + "Can't create publish job without prior ppducing jobs first") + + publish_job = self.get_job(instance, instances) + + instance.data["rrJobs"] += publish_job + + metadata_path, rootless_metadata_path = self._create_metadata_path( + instance) + + self.log.info("Writing json file: {}".format(metadata_path)) + with open(metadata_path, "w") as f: + json.dump(publish_job, f, indent=4, sort_keys=True) + + def get_job(self, instance, instances): + """Create RR publishing job. + + Based on provided original instance and additional instances, + create publishing job and return it to be submitted to farm. + + Args: + instance (Instance): Original instance. + instances (list of Instance): List of instances to + be published on farm. + + Returns: + RRJob: RoyalRender publish job. + + """ data = instance.data.copy() subset = data["subset"] job_name = "Publish - {subset}".format(subset=subset) - override_version = None instance_version = instance.data.get("version") # take this if exists - if instance_version != 1: - override_version = instance_version - output_dir = self._get_publish_folder( - instance.context.data['anatomy'], - deepcopy(instance.data["anatomyData"]), - instance.data.get("asset"), - instances[0]["subset"], - # TODO: this shouldn't be hardcoded and is in fact settable by - # Settings. - 'render', - override_version - ) + override_version = instance_version if instance_version != 1 else None # Transfer the environment from the original job to this dependent # job, so they use the same environment metadata_path, roothless_metadata_path = \ - self._create_metadata_path(instance) + self._create_metadata_path(instance) environment = RREnvList({ "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], @@ -96,7 +136,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): # and collect all pre_ids to wait for job_environ = {} jobs_pre_ids = [] - for job in instance["rrJobs"]: # type: RRJob + for job in instance.data["rrJobs"]: # type: RRJob if job.rrEnvList: job_environ.update( dict(RREnvList.parse(job.rrEnvList)) @@ -159,11 +199,4 @@ class CreatePublishRoyalRenderJob(InstancePlugin): else: job.WaitForPreIDs += jobs_pre_ids - self.log.info("Creating RoyalRender Publish job ...") - - if not instance.data.get("rrJobs"): - self.log.error("There is no RoyalRender job on the instance.") - raise KnownPublishError( - "Can't create publish job without producing jobs") - - instance.data["rrJobs"] += job + return job diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index 689a488a5c..5f034e74a1 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -9,6 +9,17 @@ from collections import namedtuple, OrderedDict CustomAttribute = namedtuple("CustomAttribute", ["name", "value"]) +def get_rr_platform(): + # type: () -> str + """Returns name of platform used in rr jobs.""" + if sys.platform.lower() in ["win32", "win64"]: + return "windows" + elif sys.platform.lower() == "darwin": + return "mac" + else: + return "linux" + + class RREnvList(dict): def serialize(self): # VariableA=ValueA~~~VariableB=ValueB @@ -163,17 +174,6 @@ class RRJob: # only used in RR 8.3 and newer rrEnvList = attr.ib(default=None) # type: str - @staticmethod - def get_rr_platform(): - # type: () -> str - """Returns name of platform used in rr jobs.""" - if sys.platform.lower() in ["win32", "win64"]: - return "windows" - elif sys.platform.lower() == "darwin": - return "mac" - else: - return "linux" - class SubmitterParameter: """Wrapper for Submitter Parameters.""" diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish.py index 35a944444f..e5ebd3666c 100644 --- a/openpype/pipeline/farm/pyblish.py +++ b/openpype/pipeline/farm/pyblish.py @@ -11,10 +11,12 @@ from openpype.lib import Logger import attr import pyblish.api from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.farm.patterning import match_aov_pattern import os import clique from copy import deepcopy import re +import warnings @attr.s @@ -263,6 +265,7 @@ def create_skeleton_instance( return instance_skeleton_data + def _solve_families(families): """Solve families. @@ -277,7 +280,9 @@ def _solve_families(families): if "review" not in families: families.append("review") return families -def create_instances_for_aov(instance, skeleton): + + +def create_instances_for_aov(instance, skeleton, aov_filter): """Create instances from AOVs. This will create new pyblish.api.Instances by going over expected @@ -328,11 +333,12 @@ def create_instances_for_aov(instance, skeleton): return _create_instances_for_aov( instance, skeleton, + aov_filter, additional_color_data ) -def _create_instances_for_aov(instance, skeleton, additional_data): +def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data): """Create instance for each AOV found. This will create new instance for every AOV it can detect in expected @@ -491,35 +497,64 @@ def _create_instances_for_aov(instance, skeleton, additional_data): log.debug("instances:{}".format(instances)) return instances -def get_resources(project_name, version, extension=None): - """Get the files from the specific version.""" - # TODO this functions seems to be weird - # - it's looking for representation with one extension or first (any) - # representation from a version? - # - not sure how this should work, maybe it does for specific use cases - # but probably can't be used for all resources from 2D workflows - extensions = None +def get_resources(project_name, version, extension=None): + """Get the files from the specific version. + + This will return all get all files from representation. + + Todo: + This is really weird function, and it's use is + highly controversial. First, it will not probably work + ar all in final release of AYON, second, the logic isn't sound. + It should try to find representation matching the current one - + because it is used to pull out files from previous version to + be included in this one. + + .. deprecated:: 3.15.5 + This won't work in AYON and even the logic must be refactored. + + Args: + project_name (str): Name of the project. + version (dict): Version document. + extension (str): extension used to filter + representations. + + Returns: + list: of files + + """ + warnings.warn(( + "This won't work in AYON and even " + "the logic must be refactored."), DeprecationWarning) + extensions = [] if extension: extensions = [extension] - repre_docs = list(get_representations( - project_name, version_ids=[version["_id"]], extensions=extensions - )) - assert repre_docs, "This is a bug" - representation = repre_docs[0] + # there is a `context_filter` argument that won't probably work in + # final release of AYON. SO we'll rather not use it + repre_docs = list(get_representations( + project_name, version_ids=[version["_id"]])) + + filtered = [] + for doc in repre_docs: + if doc["context"]["ext"] in extensions: + filtered.append(doc) + + representation = filtered[0] directory = get_representation_path(representation) print("Source: ", directory) resources = sorted( [ - os.path.normpath(os.path.join(directory, fname)) - for fname in os.listdir(directory) + os.path.normpath(os.path.join(directory, file_name)) + for file_name in os.listdir(directory) ] ) return resources -def copy_extend_frames(self, instance, representation): + +def copy_extend_frames(instance, representation): """Copy existing frames from latest version. This will copy all existing frames from subset's latest version back @@ -533,11 +568,15 @@ def copy_extend_frames(self, instance, representation): """ import speedcopy + R_FRAME_NUMBER = re.compile( + r".+\.(?P[0-9]+)\..+") + log = Logger.get_logger("farm_publishing") log.info("Preparing to copy ...") start = instance.data.get("frameStart") end = instance.data.get("frameEnd") project_name = instance.context.data["project"] + anatomy = instance.data["anatomy"] # type: Anatomy # get latest version of subset # this will stop if subset wasn't published yet @@ -554,7 +593,7 @@ def copy_extend_frames(self, instance, representation): ) r_col, _ = clique.assemble(subset_resources) - # if override remove all frames we are expecting to be rendered + # if override remove all frames we are expecting to be rendered, # so we'll copy only those missing from current render if instance.data.get("overrideExistingFrame"): for frame in range(start, end + 1): @@ -568,18 +607,18 @@ def copy_extend_frames(self, instance, representation): resource_files = [] r_filename = os.path.basename( representation.get("files")[0]) # first file - op = re.search(self.R_FRAME_NUMBER, r_filename) + op = re.search(R_FRAME_NUMBER, r_filename) pre = r_filename[:op.start("frame")] post = r_filename[op.end("frame"):] assert op is not None, "padding string wasn't found" for frame in list(r_col): - fn = re.search(self.R_FRAME_NUMBER, frame) + fn = re.search(R_FRAME_NUMBER, frame) # silencing linter as we need to compare to True, not to # type assert fn is not None, "padding string wasn't found" # list of tuples (source, destination) staging = representation.get("stagingDir") - staging = self.anatomy.fill_root(staging) + staging = anatomy.fill_root(staging) resource_files.append( (frame, os.path.join( staging, "{}{}{}".format(pre, fn["frame"], post))) @@ -596,3 +635,34 @@ def copy_extend_frames(self, instance, representation): log.info(" > {}".format(source[1])) log.info("Finished copying %i files" % len(resource_files)) + + +def attach_instances_to_subset(attach_to, instances): + """Attach instance to subset. + + If we are attaching to other subsets, create copy of existing + instances, change data to match its subset and replace + existing instances with modified data. + + Args: + attach_to (list): List of instances to attach to. + instances (list): List of instances to attach. + + Returns: + list: List of attached instances. + + """ + # + + new_instances = [] + for attach_instance in attach_to: + for i in instances: + new_inst = copy(i) + new_inst["version"] = attach_instance.get("version") + new_inst["subset"] = attach_instance.get("subset") + new_inst["family"] = attach_instance.get("family") + new_inst["append"] = True + # don't set subsetGroup if we are attaching + new_inst.pop("subsetGroup") + new_instances.append(new_inst) + return new_instances diff --git a/openpype/pipeline/farm/pyblish.pyi b/openpype/pipeline/farm/pyblish.pyi index 3667f2d8a5..76f7c34dcd 100644 --- a/openpype/pipeline/farm/pyblish.pyi +++ b/openpype/pipeline/farm/pyblish.pyi @@ -20,4 +20,5 @@ def extend_frames(asset: str, subset: str, start: int, end: int) -> Tuple[int, i def get_time_data_from_instance_or_context(instance: pyblish.api.Instance) -> TimeData: ... def get_transferable_representations(instance: pyblish.api.Instance) -> list: ... def create_skeleton_instance(instance: pyblish.api.Instance, families_transfer: list = ..., instance_transfer: dict = ...) -> dict: ... -def create_instances_for_aov(instance: pyblish.api.Instance, skeleton: dict) -> List[pyblish.api.Instance]: ... +def create_instances_for_aov(instance: pyblish.api.Instance, skeleton: dict, aov_filter: dict) -> List[pyblish.api.Instance]: ... +def attach_instances_to_subset(attach_to: list, instances: list) -> list: ... From 1f0572aaa0c1e69f8a6b5cf33d20546029bf6f53 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 28 Apr 2023 18:37:56 +0200 Subject: [PATCH 047/446] :recycle: refactor deadline code to make use of abstracted code --- .../plugins/publish/submit_publish_job.py | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 58f34c24b1..86f647dd1b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -23,7 +23,8 @@ from openpype.pipeline.farm.patterning import match_aov_pattern from openpype.lib import is_running_from_build from openpype.pipeline.farm.pyblish import ( create_skeleton_instance, - create_instances_for_aov + create_instances_for_aov, + attach_instances_to_subset ) @@ -551,7 +552,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if isinstance(instance.data.get("expectedFiles")[0], dict): instances = create_instances_for_aov( - instance, instance_skeleton_data) + instance, instance_skeleton_data, self.aov_filter) else: representations = self._get_representations( @@ -566,25 +567,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): instance_skeleton_data["representations"] += representations instances = [instance_skeleton_data] - # if we are attaching to other subsets, create copy of existing - # instances, change data to match its subset and replace - # existing instances with modified data + # attach instances to subset if instance.data.get("attachTo"): - self.log.info("Attaching render to subset:") - new_instances = [] - for at in instance.data.get("attachTo"): - for i in instances: - new_i = copy(i) - new_i["version"] = at.get("version") - new_i["subset"] = at.get("subset") - new_i["family"] = at.get("family") - new_i["append"] = True - # don't set subsetGroup if we are attaching - new_i.pop("subsetGroup") - new_instances.append(new_i) - self.log.info(" - {} / v{}".format( - at.get("subset"), at.get("version"))) - instances = new_instances + instances = attach_instances_to_subset( + instance.data.get("attachTo"), instances + ) r''' SUBMiT PUBLiSH JOB 2 D34DLiN3 ____ From 806b9250c89838b84a33b8fd24849a62dd1f35b7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 May 2023 18:32:46 +0200 Subject: [PATCH 048/446] Fix wrong family 'rendering' is obsolete --- .../plugins/publish/collect_rr_path_from_instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index cfb5b78077..e21cd7c39f 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -7,7 +7,7 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder label = "Collect Royal Render path name from the Instance" - families = ["rendering"] + families = ["render"] def process(self, instance): instance.data["rrPathName"] = self._collect_rr_path_name(instance) From ddab9240a1d1c51c06787ae24f9d17767bdb7a28 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 May 2023 18:35:22 +0200 Subject: [PATCH 049/446] Extract iter_expected_files --- .../plugins/publish/submit_maya_deadline.py | 15 +++------------ .../publish/create_maya_royalrender_job.py | 13 ++----------- .../plugins/publish/create_nuke_deadline_job.py | 7 +++++-- openpype/pipeline/farm/tools.py | 10 ++++++++++ 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a6cdcb7e71..16fba382e4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -39,6 +39,7 @@ from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo from openpype.tests.lib import is_in_tests from openpype.lib import is_running_from_build +from openpype.pipeline.farm.tools import iter_expected_files def _validate_deadline_bool_value(instance, attribute, value): @@ -198,7 +199,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): # Add list of expected files to job # --------------------------------- exp = instance.data.get("expectedFiles") - for filepath in self._iter_expected_files(exp): + for filepath in iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -254,7 +255,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): # TODO: Avoid the need for this logic here, needed for submit publish # Store output dir for unified publisher (filesequence) expected_files = instance.data["expectedFiles"] - first_file = next(self._iter_expected_files(expected_files)) + first_file = next(iter_expected_files(expected_files)) output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" @@ -772,16 +773,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): end=int(self._instance.data["frameEndHandle"]), ) - @staticmethod - def _iter_expected_files(exp): - if isinstance(exp[0], dict): - for _aov, files in exp[0].items(): - for file in files: - yield file - else: - for file in exp: - yield file - def _format_tiles( filename, diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 6e23fb0b74..8461e74d6d 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -11,6 +11,7 @@ from openpype.pipeline.publish.lib import get_published_workfile_instance from openpype.pipeline.publish import KnownPublishError from openpype.modules.royalrender.api import Api as rrApi from openpype.modules.royalrender.rr_job import RRJob, CustomAttribute +from openpype.pipeline.farm.tools import iter_expected_files class CreateMayaRoyalRenderJob(InstancePlugin): @@ -43,7 +44,7 @@ class CreateMayaRoyalRenderJob(InstancePlugin): return "linux" expected_files = self._instance.data["expectedFiles"] - first_file = next(self._iter_expected_files(expected_files)) + first_file = next(iter_expected_files(expected_files)) output_dir = os.path.dirname(first_file) self._instance.data["outputDir"] = output_dir workspace = self._instance.context.data["workspaceDir"] @@ -125,16 +126,6 @@ class CreateMayaRoyalRenderJob(InstancePlugin): self._instance.data["rrJobs"] += self.get_job() - @staticmethod - def _iter_expected_files(exp): - if isinstance(exp[0], dict): - for _aov, files in exp[0].items(): - for file in files: - yield file - else: - for file in exp: - yield file - @staticmethod def _resolve_rr_path(context, rr_path_name): # type: (Context, str) -> str diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py index 9f49294459..217ebed057 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py @@ -19,9 +19,11 @@ from openpype.lib import ( BoolDef, NumberDef ) +from openpype.pipeline import OpenPypePyblishPluginMixin +from openpype.pipeline.farm.tools import iter_expected_files -class CreateNukeRoyalRenderJob(InstancePlugin): +class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): label = "Create Nuke Render job in RR" order = IntegratorOrder + 0.1 hosts = ["nuke"] @@ -208,7 +210,8 @@ class CreateNukeRoyalRenderJob(InstancePlugin): # this will append expected files to instance as needed. expected_files = self.expected_files( render_path, start_frame, end_frame) - first_file = next(self._iter_expected_files(expected_files)) + self._instance.data["expectedFiles"].extend(expected_files) + first_file = next(iter_expected_files(expected_files)) job = RRJob( Software="Nuke", diff --git a/openpype/pipeline/farm/tools.py b/openpype/pipeline/farm/tools.py index 506f95d6b2..6f9e0ac393 100644 --- a/openpype/pipeline/farm/tools.py +++ b/openpype/pipeline/farm/tools.py @@ -100,3 +100,13 @@ def from_published_scene(instance, replace_in_path=True): instance.data["publishRenderMetadataFolder"] = metadata_folder return file_path + + +def iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file From 7fe4820bec138f5f740793f3d51a346176dece49 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 May 2023 18:37:49 +0200 Subject: [PATCH 050/446] Renamed file Original name collided in Nuke (Python2). --- .../farm/{pyblish.py => pyblish_functions.py} | 20 ++++++++++--------- .../{pyblish.pyi => pyblish_functions.pyi} | 0 2 files changed, 11 insertions(+), 9 deletions(-) rename openpype/pipeline/farm/{pyblish.py => pyblish_functions.py} (99%) rename openpype/pipeline/farm/{pyblish.pyi => pyblish_functions.pyi} (100%) diff --git a/openpype/pipeline/farm/pyblish.py b/openpype/pipeline/farm/pyblish_functions.py similarity index 99% rename from openpype/pipeline/farm/pyblish.py rename to openpype/pipeline/farm/pyblish_functions.py index e5ebd3666c..84d958bea1 100644 --- a/openpype/pipeline/farm/pyblish.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -1,3 +1,12 @@ +import copy +import attr +from pyblish.api import Instance +import os +import clique +from copy import deepcopy +import re +import warnings + from openpype.pipeline import ( get_current_project_name, get_representation_path, @@ -8,19 +17,12 @@ from openpype.client import ( get_representations ) from openpype.lib import Logger -import attr -import pyblish.api from openpype.pipeline.publish import KnownPublishError from openpype.pipeline.farm.patterning import match_aov_pattern -import os -import clique -from copy import deepcopy -import re -import warnings @attr.s -class TimeData: +class TimeData(object): """Structure used to handle time related data.""" start = attr.ib(type=int) end = attr.ib(type=int) @@ -160,7 +162,7 @@ def get_transferable_representations(instance): def create_skeleton_instance( instance, families_transfer=None, instance_transfer=None): - # type: (pyblish.api.Instance, list, dict) -> dict + # type: (Instance, list, dict) -> dict """Create skeleton instance from original instance data. This will create dictionary containing skeleton diff --git a/openpype/pipeline/farm/pyblish.pyi b/openpype/pipeline/farm/pyblish_functions.pyi similarity index 100% rename from openpype/pipeline/farm/pyblish.pyi rename to openpype/pipeline/farm/pyblish_functions.pyi From 266d34bebb1e1ab07554edadee556da14a2d84a5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 May 2023 18:38:55 +0200 Subject: [PATCH 051/446] Fix querying anatomy Should be from context, not on instance anymore. --- openpype/pipeline/farm/pyblish_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 84d958bea1..a2eaddbba6 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -136,7 +136,7 @@ def get_transferable_representations(instance): list of dicts: List of transferable representations. """ - anatomy = instance.data.get("anatomy") # type: Anatomy + anatomy = instance.context.data["anatomy"] # type: Anatomy to_transfer = [] for representation in instance.data.get("representations", []): @@ -187,7 +187,7 @@ def create_skeleton_instance( context = instance.context data = instance.data.copy() - anatomy = data["anatomy"] # type: Anatomy + anatomy = instance.context.data["anatomy"] # type: Anatomy families = [data["family"]] @@ -363,7 +363,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data): # TODO: this needs to be taking the task from context or instance task = os.environ["AVALON_TASK"] - anatomy = instance.data["anatomy"] + anatomy = instance.context.data["anatomy"] subset = skeleton["subset"] cameras = instance.data.get("cameras", []) exp_files = instance.data["expectedFiles"] @@ -578,7 +578,7 @@ def copy_extend_frames(instance, representation): start = instance.data.get("frameStart") end = instance.data.get("frameEnd") project_name = instance.context.data["project"] - anatomy = instance.data["anatomy"] # type: Anatomy + anatomy = instance.context.data["anatomy"] # type: Anatomy # get latest version of subset # this will stop if subset wasn't published yet From 79132763ab9bd4cb00b1e21bb8ace6381120d01a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 May 2023 18:39:29 +0200 Subject: [PATCH 052/446] Fix typo, wrong parentheses --- openpype/pipeline/farm/pyblish_functions.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index a2eaddbba6..f4c1ea2f27 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -109,16 +109,16 @@ def get_time_data_from_instance_or_context(instance): """ return TimeData( - start=instance.data.get["frameStart"] or \ - instance.context.data.get("frameStart"), - end=instance.data.get("frameEnd") or \ - instance.context.data.get("frameEnd"), - fps=instance.data.get("fps") or \ - instance.context.data.get("fps"), - handle_start=instance.data.get("handleStart") or \ - instance.context.data.get("handleStart"), # noqa: E501 - handle_end=instance.data.get("handleStart") or \ - instance.context.data.get("handleStart") + start=(instance.data.get("frameStart") or + instance.context.data.get("frameStart")), + end=(instance.data.get("frameEnd") or + instance.context.data.get("frameEnd")), + fps=(instance.data.get("fps") or + instance.context.data.get("fps")), + handle_start=(instance.data.get("handleStart") or + instance.context.data.get("handleStart")), # noqa: E501 + handle_end=(instance.data.get("handleStart") or + instance.context.data.get("handleStart")) ) From 87eb1edf891fb8f8cfe968654dc65ace58afbd84 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 May 2023 18:40:47 +0200 Subject: [PATCH 053/446] Fix wrong method --- openpype/pipeline/farm/pyblish_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index f4c1ea2f27..645b31b2de 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -659,7 +659,7 @@ def attach_instances_to_subset(attach_to, instances): new_instances = [] for attach_instance in attach_to: for i in instances: - new_inst = copy(i) + new_inst = copy.deepcopy(i) new_inst["version"] = attach_instance.get("version") new_inst["subset"] = attach_instance.get("subset") new_inst["family"] = attach_instance.get("family") From c284908cb42244236a8d439b6e7178c101b715f3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 May 2023 18:48:36 +0200 Subject: [PATCH 054/446] Fix attr.s for Python2 --- openpype/modules/royalrender/rr_job.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index 5f034e74a1..8d96b8ff4a 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -38,7 +38,7 @@ class RREnvList(dict): @attr.s -class RRJob: +class RRJob(object): """Mapping of Royal Render job file to a data class.""" # Required @@ -197,7 +197,7 @@ class SubmitterParameter: @attr.s -class SubmitFile: +class SubmitFile(object): """Class wrapping Royal Render submission XML file.""" # Syntax version of the submission file. From 1b4f452301436131c0024e80b5b80538861ceba8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 May 2023 19:23:58 +0200 Subject: [PATCH 055/446] Extracted prepare_representations Various fixes. WIP --- .../plugins/publish/submit_publish_job.py | 149 ++--------------- .../publish/create_nuke_deadline_job.py | 48 +++++- .../publish/create_publish_royalrender_job.py | 32 +++- .../publish/submit_jobs_to_royalrender.py | 2 +- openpype/pipeline/farm/pyblish_functions.py | 156 +++++++++++++++++- 5 files changed, 230 insertions(+), 157 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 86f647dd1b..58a0cd7219 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -21,10 +21,11 @@ from openpype.pipeline import ( from openpype.tests.lib import is_in_tests from openpype.pipeline.farm.patterning import match_aov_pattern from openpype.lib import is_running_from_build -from openpype.pipeline.farm.pyblish import ( +from openpype.pipeline.farm.pyblish_functions import ( create_skeleton_instance, create_instances_for_aov, - attach_instances_to_subset + attach_instances_to_subset, + prepare_representations ) @@ -332,140 +333,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): return deadline_publish_job_id - def _get_representations(self, instance, exp_files): - """Create representations for file sequences. - - This will return representations of expected files if they are not - in hierarchy of aovs. There should be only one sequence of files for - most cases, but if not - we create representation from each of them. - - Arguments: - instance (dict): instance data for which we are - setting representations - exp_files (list): list of expected files - - Returns: - list of representations - - """ - representations = [] - host_name = os.environ.get("AVALON_APP", "") - collections, remainders = clique.assemble(exp_files) - - # create representation for every collected sequence - for collection in collections: - ext = collection.tail.lstrip(".") - preview = False - # TODO 'useSequenceForReview' is temporary solution which does - # not work for 100% of cases. We must be able to tell what - # expected files contains more explicitly and from what - # should be review made. - # - "review" tag is never added when is set to 'False' - if instance["useSequenceForReview"]: - # toggle preview on if multipart is on - if instance.get("multipartExr", False): - self.log.debug( - "Adding preview tag because its multipartExr" - ) - preview = True - else: - render_file_name = list(collection)[0] - # if filtered aov name is found in filename, toggle it for - # preview video rendering - preview = match_aov_pattern( - host_name, self.aov_filter, render_file_name - ) - - staging = os.path.dirname(list(collection)[0]) - success, rootless_staging_dir = ( - self.anatomy.find_root_template_from_path(staging) - ) - if success: - staging = rootless_staging_dir - else: - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(staging)) - - frame_start = int(instance.get("frameStartHandle")) - if instance.get("slate"): - frame_start -= 1 - - rep = { - "name": ext, - "ext": ext, - "files": [os.path.basename(f) for f in list(collection)], - "frameStart": frame_start, - "frameEnd": int(instance.get("frameEndHandle")), - # If expectedFile are absolute, we need only filenames - "stagingDir": staging, - "fps": instance.get("fps"), - "tags": ["review"] if preview else [], - } - - # poor man exclusion - if ext in self.skip_integration_repre_list: - rep["tags"].append("delete") - - if instance.get("multipartExr", False): - rep["tags"].append("multipartExr") - - # support conversion from tiled to scanline - if instance.get("convertToScanline"): - self.log.info("Adding scanline conversion.") - rep["tags"].append("toScanline") - - representations.append(rep) - - self._solve_families(instance, preview) - - # add remainders as representations - for remainder in remainders: - ext = remainder.split(".")[-1] - - staging = os.path.dirname(remainder) - success, rootless_staging_dir = ( - self.anatomy.find_root_template_from_path(staging) - ) - if success: - staging = rootless_staging_dir - else: - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(staging)) - - rep = { - "name": ext, - "ext": ext, - "files": os.path.basename(remainder), - "stagingDir": staging, - } - - preview = match_aov_pattern( - host_name, self.aov_filter, remainder - ) - if preview: - rep.update({ - "fps": instance.get("fps"), - "tags": ["review"] - }) - self._solve_families(instance, preview) - - already_there = False - for repre in instance.get("representations", []): - # might be added explicitly before by publish_on_farm - already_there = repre.get("files") == rep["files"] - if already_there: - self.log.debug("repre {} already_there".format(repre)) - break - - if not already_there: - representations.append(rep) - - return representations - def _solve_families(self, instance, preview=False): families = instance.get("families") @@ -552,12 +419,16 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if isinstance(instance.data.get("expectedFiles")[0], dict): instances = create_instances_for_aov( - instance, instance_skeleton_data, self.aov_filter) + instance, instance_skeleton_data, + self.aov_filter, self.skip_integration_repre_list) else: - representations = self._get_representations( + representations = prepare_representations( instance_skeleton_data, - instance.data.get("expectedFiles") + instance.data.get("expectedFiles"), + self.anatomy, + self.aov_filter, + self.skip_integration_repre_list ) if "representations" not in instance_skeleton_data.keys(): diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py index 217ebed057..0472f2ea80 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- """Submitting render job to RoyalRender.""" +import copy import os -import sys import re import platform from datetime import datetime from pyblish.api import InstancePlugin, IntegratorOrder, Context from openpype.tests.lib import is_in_tests -from openpype.lib import is_running_from_build from openpype.pipeline.publish.lib import get_published_workfile_instance from openpype.pipeline.publish import KnownPublishError from openpype.modules.royalrender.api import Api as rrApi @@ -34,6 +33,8 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): priority = 50 chunk_size = 1 concurrent_tasks = 1 + use_gpu = True + use_published = True @classmethod def get_attribute_defs(cls): @@ -69,6 +70,11 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): "suspend_publish", default=False, label="Suspend publish" + ), + BoolDef( + "use_published", + default=cls.use_published, + label="Use published workfile" ) ] @@ -81,6 +87,17 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): self.rr_api = None def process(self, instance): + # import json + # def _default_json(value): + # return str(value) + # filepath = "C:\\Users\\petrk\\PycharmProjects\\Pype3.0\\pype\\tests\\unit\\openpype\\modules\\royalrender\\plugins\\publish\\resources\\instance.json" + # with open(filepath, "w") as f: + # f.write(json.dumps(instance.data, indent=4, default=_default_json)) + # + # filepath = "C:\\Users\\petrk\\PycharmProjects\\Pype3.0\\pype\\tests\\unit\\openpype\\modules\\royalrender\\plugins\\publish\\resources\\context.json" + # with open(filepath, "w") as f: + # f.write(json.dumps(instance.context.data, indent=4, default=_default_json)) + if not instance.data.get("farm"): self.log.info("Skipping local instance.") return @@ -93,6 +110,7 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): "suspend_publish"] context = instance.context + self._instance = instance self._rr_root = self._resolve_rr_path(context, instance.data.get( "rrPathName")) # noqa @@ -218,7 +236,7 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): Renderer="", SeqStart=int(start_frame), SeqEnd=int(end_frame), - SeqStep=int(self._instance.data.get("byFrameStep"), 1), + SeqStep=int(self._instance.data.get("byFrameStep", 1)), SeqFileOffset=0, Version=nuke_version.group(), SceneName=script_path, @@ -298,7 +316,7 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): if "%" not in file: expected_files.append(path) - return + return expected_files if self._instance.data.get("slate"): start_frame -= 1 @@ -308,3 +326,25 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): for i in range(start_frame, (end_frame + 1)) ) return expected_files + + def preview_fname(self, path): + """Return output file path with #### for padding. + + Deadline requires the path to be formatted with # in place of numbers. + For example `/path/to/render.####.png` + + Args: + path (str): path to rendered images + + Returns: + str + + """ + self.log.debug("_ path: `{}`".format(path)) + if "%" in path: + search_results = re.search(r"(%0)(\d)(d.)", path).groups() + self.log.debug("_ search_results: `{}`".format(search_results)) + return int(search_results[1]) + if "#" in path: + self.log.debug("_ path: `{}`".format(path)) + return path diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index b1c84c87b9..6f0bc995d0 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -11,10 +11,11 @@ from openpype.modules.royalrender.rr_job import RRJob, RREnvList from openpype.pipeline.publish import KnownPublishError from openpype.lib.openpype_version import ( get_OpenPypeVersion, get_openpype_version) -from openpype.pipeline.farm.pyblish import ( +from openpype.pipeline.farm.pyblish_functions import ( create_skeleton_instance, create_instances_for_aov, - attach_instances_to_subset + attach_instances_to_subset, + prepare_representations ) @@ -31,6 +32,20 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "harmony": [r".*"], # for everything from AE "celaction": [r".*"]} + skip_integration_repre_list = [] + + # mapping of instance properties to be transferred to new instance + # for every specified family + instance_transfer = { + "slate": ["slateFrames", "slate"], + "review": ["lutPath"], + "render2d": ["bakingNukeScripts", "version"], + "renderlayer": ["convertToScanline"] + } + + # list of family names to transfer to new family if present + families_transfer = ["render3d", "render2d", "ftrack", "slate"] + def process(self, instance): # data = instance.data.copy() context = instance.context @@ -46,15 +61,18 @@ class CreatePublishRoyalRenderJob(InstancePlugin): families_transfer=self.families_transfer, instance_transfer=self.instance_transfer) - instances = None if isinstance(instance.data.get("expectedFiles")[0], dict): instances = create_instances_for_aov( - instance, instance_skeleton_data, self.aov_filter) + instance, instance_skeleton_data, + self.aov_filter, self.skip_integration_repre_list) else: - representations = self._get_representations( + representations = prepare_representations( instance_skeleton_data, - instance.data.get("expectedFiles") + instance.data.get("expectedFiles"), + self.anatomy, + self.aov_filter, + self.skip_integration_repre_list ) if "representations" not in instance_skeleton_data.keys(): @@ -104,7 +122,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): RRJob: RoyalRender publish job. """ - data = instance.data.copy() + data = deepcopy(instance.data) subset = data["subset"] job_name = "Publish - {subset}".format(subset=subset) diff --git a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py index 325fb36993..8546554372 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -11,7 +11,7 @@ from openpype.pipeline.publish import KnownPublishError class SubmitJobsToRoyalRender(ContextPlugin): """Find all jobs, create submission XML and submit it to RoyalRender.""" label = "Submit jobs to RoyalRender" - order = IntegratorOrder + 0.1 + order = IntegratorOrder + 0.3 targets = ["local"] def __init__(self): diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 645b31b2de..792cc07f02 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -284,7 +284,150 @@ def _solve_families(families): return families -def create_instances_for_aov(instance, skeleton, aov_filter): +def prepare_representations(instance, exp_files, anatomy, aov_filter, + skip_integration_repre_list): + """Create representations for file sequences. + + This will return representations of expected files if they are not + in hierarchy of aovs. There should be only one sequence of files for + most cases, but if not - we create representation from each of them. + + Arguments: + instance (dict): instance data for which we are + setting representations + exp_files (list): list of expected files + anatomy (Anatomy): + aov_filter (dict): add review for specific aov names + skip_integration_repre_list (list): exclude specific extensions + + Returns: + list of representations + + """ + representations = [] + host_name = os.environ.get("AVALON_APP", "") + collections, remainders = clique.assemble(exp_files) + + log = Logger.get_logger("farm_publishing") + + # create representation for every collected sequence + for collection in collections: + ext = collection.tail.lstrip(".") + preview = False + # TODO 'useSequenceForReview' is temporary solution which does + # not work for 100% of cases. We must be able to tell what + # expected files contains more explicitly and from what + # should be review made. + # - "review" tag is never added when is set to 'False' + if instance["useSequenceForReview"]: + # toggle preview on if multipart is on + if instance.get("multipartExr", False): + log.debug( + "Adding preview tag because its multipartExr" + ) + preview = True + else: + render_file_name = list(collection)[0] + # if filtered aov name is found in filename, toggle it for + # preview video rendering + preview = match_aov_pattern( + host_name, aov_filter, render_file_name + ) + + staging = os.path.dirname(list(collection)[0]) + success, rootless_staging_dir = ( + anatomy.find_root_template_from_path(staging) + ) + if success: + staging = rootless_staging_dir + else: + log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(staging)) + + frame_start = int(instance.get("frameStartHandle")) + if instance.get("slate"): + frame_start -= 1 + + rep = { + "name": ext, + "ext": ext, + "files": [os.path.basename(f) for f in list(collection)], + "frameStart": frame_start, + "frameEnd": int(instance.get("frameEndHandle")), + # If expectedFile are absolute, we need only filenames + "stagingDir": staging, + "fps": instance.get("fps"), + "tags": ["review"] if preview else [], + } + + # poor man exclusion + if ext in skip_integration_repre_list: + rep["tags"].append("delete") + + if instance.get("multipartExr", False): + rep["tags"].append("multipartExr") + + # support conversion from tiled to scanline + if instance.get("convertToScanline"): + log.info("Adding scanline conversion.") + rep["tags"].append("toScanline") + + representations.append(rep) + + if preview: + instance["families"] = _solve_families(instance["families"]) + + # add remainders as representations + for remainder in remainders: + ext = remainder.split(".")[-1] + + staging = os.path.dirname(remainder) + success, rootless_staging_dir = ( + anatomy.find_root_template_from_path(staging) + ) + if success: + staging = rootless_staging_dir + else: + log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(staging)) + + rep = { + "name": ext, + "ext": ext, + "files": os.path.basename(remainder), + "stagingDir": staging, + } + + preview = match_aov_pattern( + host_name, aov_filter, remainder + ) + if preview: + rep.update({ + "fps": instance.get("fps"), + "tags": ["review"] + }) + instance["families"] = _solve_families(instance["families"]) + + already_there = False + for repre in instance.get("representations", []): + # might be added explicitly before by publish_on_farm + already_there = repre.get("files") == rep["files"] + if already_there: + log.debug("repre {} already_there".format(repre)) + break + + if not already_there: + representations.append(rep) + + return representations + + +def create_instances_for_aov(instance, skeleton, aov_filter, + skip_integration_repre_list): """Create instances from AOVs. This will create new pyblish.api.Instances by going over expected @@ -336,11 +479,13 @@ def create_instances_for_aov(instance, skeleton, aov_filter): instance, skeleton, aov_filter, - additional_color_data + additional_color_data, + skip_integration_repre_list ) -def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data): +def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, + skip_integration_repre_list): """Create instance for each AOV found. This will create new instance for every AOV it can detect in expected @@ -427,7 +572,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data): render_file_name = os.path.basename(col[0]) else: render_file_name = os.path.basename(col) - aov_patterns = self.aov_filter + aov_patterns = aov_filter preview = match_aov_pattern(app, aov_patterns, render_file_name) # toggle preview on if multipart is on @@ -436,7 +581,6 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data): log.debug("Adding preview tag because its multipartExr") preview = True - new_instance = deepcopy(skeleton) new_instance["subsetGroup"] = group_name if preview: @@ -483,7 +627,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data): rep["tags"].append("toScanline") # poor man exclusion - if ext in self.skip_integration_repre_list: + if ext in skip_integration_repre_list: rep["tags"].append("delete") if preview: From fb06a2e6819ae1e7696306c48457c9d6f681ab07 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 12:59:46 +0200 Subject: [PATCH 056/446] Extracted create_metadata_path --- .../plugins/publish/submit_publish_job.py | 43 +++---------------- .../publish/create_publish_royalrender_job.py | 12 +++--- openpype/pipeline/farm/pyblish_functions.py | 33 ++++++++++++++ 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 58a0cd7219..ff6bcf1801 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Submit publishing job to farm.""" - import os import json import re @@ -12,20 +11,18 @@ import pyblish.api from openpype.client import ( get_last_version_by_subset_name, - get_representations, ) from openpype.pipeline import ( - get_representation_path, legacy_io, ) from openpype.tests.lib import is_in_tests -from openpype.pipeline.farm.patterning import match_aov_pattern from openpype.lib import is_running_from_build from openpype.pipeline.farm.pyblish_functions import ( create_skeleton_instance, create_instances_for_aov, attach_instances_to_subset, - prepare_representations + prepare_representations, + create_metadata_path ) @@ -154,36 +151,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # poor man exclusion skip_integration_repre_list = [] - def _create_metadata_path(self, instance): - ins_data = instance.data - # Ensure output dir exists - output_dir = ins_data.get( - "publishRenderMetadataFolder", ins_data["outputDir"]) - - try: - if not os.path.isdir(output_dir): - os.makedirs(output_dir) - except OSError: - # directory is not available - self.log.warning("Path is unreachable: `{}`".format(output_dir)) - - metadata_filename = "{}_metadata.json".format(ins_data["subset"]) - - metadata_path = os.path.join(output_dir, metadata_filename) - - # Convert output dir to `{root}/rest/of/path/...` with Anatomy - success, rootless_mtdt_p = self.anatomy.find_root_template_from_path( - metadata_path) - if not success: - # `rootless_path` is not set to `output_dir` if none of roots match - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(output_dir)) - rootless_mtdt_p = metadata_path - - return metadata_path, rootless_mtdt_p - def _submit_deadline_post_job(self, instance, job, instances): """Submit publish job to Deadline. @@ -216,7 +183,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Transfer the environment from the original job to this dependent # job so they use the same environment metadata_path, rootless_metadata_path = \ - self._create_metadata_path(instance) + create_metadata_path(instance, self.anatomy) environment = { "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], @@ -539,8 +506,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): } publish_job.update({"ftrack": ftrack}) - metadata_path, rootless_metadata_path = self._create_metadata_path( - instance) + metadata_path, rootless_metadata_path = \ + create_metadata_path(instance, self.anatomy) self.log.info("Writing json file: {}".format(metadata_path)) with open(metadata_path, "w") as f: diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 6f0bc995d0..5a64e5a9b7 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -15,7 +15,8 @@ from openpype.pipeline.farm.pyblish_functions import ( create_skeleton_instance, create_instances_for_aov, attach_instances_to_subset, - prepare_representations + prepare_representations, + create_metadata_path ) @@ -100,8 +101,8 @@ class CreatePublishRoyalRenderJob(InstancePlugin): instance.data["rrJobs"] += publish_job - metadata_path, rootless_metadata_path = self._create_metadata_path( - instance) + metadata_path, rootless_metadata_path = \ + create_metadata_path(instance, self.anatomy) self.log.info("Writing json file: {}".format(metadata_path)) with open(metadata_path, "w") as f: @@ -122,7 +123,8 @@ class CreatePublishRoyalRenderJob(InstancePlugin): RRJob: RoyalRender publish job. """ - data = deepcopy(instance.data) + # data = deepcopy(instance.data) + data = instance.data subset = data["subset"] job_name = "Publish - {subset}".format(subset=subset) @@ -132,7 +134,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): # Transfer the environment from the original job to this dependent # job, so they use the same environment metadata_path, roothless_metadata_path = \ - self._create_metadata_path(instance) + create_metadata_path(instance, self.anatomy) environment = RREnvList({ "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 792cc07f02..6c08545b1b 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -812,3 +812,36 @@ def attach_instances_to_subset(attach_to, instances): new_inst.pop("subsetGroup") new_instances.append(new_inst) return new_instances + + +def create_metadata_path(instance, anatomy): + ins_data = instance.data + # Ensure output dir exists + output_dir = ins_data.get( + "publishRenderMetadataFolder", ins_data["outputDir"]) + + log = Logger.get_logger("farm_publishing") + + try: + if not os.path.isdir(output_dir): + os.makedirs(output_dir) + except OSError: + # directory is not available + log.warning("Path is unreachable: `{}`".format(output_dir)) + + metadata_filename = "{}_metadata.json".format(ins_data["subset"]) + + metadata_path = os.path.join(output_dir, metadata_filename) + + # Convert output dir to `{root}/rest/of/path/...` with Anatomy + success, rootless_mtdt_p = anatomy.find_root_template_from_path( + metadata_path) + if not success: + # `rootless_path` is not set to `output_dir` if none of roots match + log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(output_dir)) + rootless_mtdt_p = metadata_path + + return metadata_path, rootless_mtdt_p From dded3e1369c365e13035484209336232b35c11ff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 14:01:11 +0200 Subject: [PATCH 057/446] Added missing variables --- .../publish/create_publish_royalrender_job.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 5a64e5a9b7..334f3a5718 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -47,6 +47,20 @@ class CreatePublishRoyalRenderJob(InstancePlugin): # list of family names to transfer to new family if present families_transfer = ["render3d", "render2d", "ftrack", "slate"] + environ_job_filter = [ + "OPENPYPE_METADATA_FILE" + ] + + environ_keys = [ + "FTRACK_API_USER", + "FTRACK_API_KEY", + "FTRACK_SERVER", + "AVALON_APP_NAME", + "OPENPYPE_USERNAME", + "OPENPYPE_SG_USER", + ] + priority = 50 + def process(self, instance): # data = instance.data.copy() context = instance.context From 3bbab4511fd5f9f30dc46ab542e79dd92e96af14 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 14:01:58 +0200 Subject: [PATCH 058/446] Removed usage of legacy_io --- .../plugins/publish/create_publish_royalrender_job.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 334f3a5718..0f0001aced 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -150,11 +150,13 @@ class CreatePublishRoyalRenderJob(InstancePlugin): metadata_path, roothless_metadata_path = \ create_metadata_path(instance, self.anatomy) + anatomy_data = instance.context.data["anatomyData"] + environment = RREnvList({ - "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], - "AVALON_ASSET": legacy_io.Session["AVALON_ASSET"], - "AVALON_TASK": legacy_io.Session["AVALON_TASK"], - "OPENPYPE_USERNAME": instance.context.data["user"], + "AVALON_PROJECT": anatomy_data["project"]["name"], + "AVALON_ASSET": anatomy_data["asset"], + "AVALON_TASK": anatomy_data["task"]["name"], + "OPENPYPE_USERNAME": anatomy_data["user"], "OPENPYPE_PUBLISH_JOB": "1", "OPENPYPE_RENDER_JOB": "0", "OPENPYPE_REMOTE_JOB": "0", From 9a95fac77c29d052facb3fce78915a85704a7eba Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 14:03:13 +0200 Subject: [PATCH 059/446] Changed to use os env OPENPYPE_VERSION get_OpenpypeVersion() returns None in hosts(or at least in Nuke) --- .../plugins/publish/create_publish_royalrender_job.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 0f0001aced..4a889ebea6 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -6,7 +6,6 @@ import json from pyblish.api import InstancePlugin, IntegratorOrder, Instance -from openpype.pipeline import legacy_io from openpype.modules.royalrender.rr_job import RRJob, RREnvList from openpype.pipeline.publish import KnownPublishError from openpype.lib.openpype_version import ( @@ -199,8 +198,6 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "--targets", "farm" ] - openpype_version = get_OpenPypeVersion() - current_version = openpype_version(version=get_openpype_version()) job = RRJob( Software="OpenPype", Renderer="Once", @@ -209,8 +206,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): SeqEnd=1, SeqStep=1, SeqFileOffset=0, - Version="{}.{}".format( - current_version.major(), current_version.minor()), + Version=os.environ.get("OPENPYPE_VERSION"), # executable SceneName=roothless_metadata_path, # command line arguments From cace4f23772cab9c0d0ad3c667a702d93b27fd93 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 14:09:50 +0200 Subject: [PATCH 060/446] Used correct function --- .../plugins/publish/create_publish_royalrender_job.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 4a889ebea6..ab1b1e881e 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -6,7 +6,11 @@ import json from pyblish.api import InstancePlugin, IntegratorOrder, Instance -from openpype.modules.royalrender.rr_job import RRJob, RREnvList +from openpype.modules.royalrender.rr_job import ( + RRJob, + RREnvList, + get_rr_platform +) from openpype.pipeline.publish import KnownPublishError from openpype.lib.openpype_version import ( get_OpenPypeVersion, get_openpype_version) @@ -216,7 +220,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): ImageDir="", ImageExtension="", ImagePreNumberLetter="", - SceneOS=RRJob.get_rr_platform(), + SceneOS=get_rr_platform(), rrEnvList=environment.serialize(), Priority=priority ) From b18e6ad431022af757a06e989205cb23fb99e761 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 14:50:42 +0200 Subject: [PATCH 061/446] Proper serialization Without it json would complain. --- .../plugins/publish/create_publish_royalrender_job.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index ab1b1e881e..602d578a4b 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Create publishing job on RoyalRender.""" import os +import attr from copy import deepcopy import json @@ -123,7 +124,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): self.log.info("Writing json file: {}".format(metadata_path)) with open(metadata_path, "w") as f: - json.dump(publish_job, f, indent=4, sort_keys=True) + json.dump(attr.asdict(publish_job), f, indent=4, sort_keys=True) def get_job(self, instance, instances): """Create RR publishing job. From 050c11ee2b30861d77805de76f5aa75b25824f14 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 14:57:53 +0200 Subject: [PATCH 062/446] Fix adding --- .../plugins/publish/create_publish_royalrender_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 602d578a4b..a74c528676 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -117,7 +117,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): publish_job = self.get_job(instance, instances) - instance.data["rrJobs"] += publish_job + instance.data["rrJobs"].append(publish_job) metadata_path, rootless_metadata_path = \ create_metadata_path(instance, self.anatomy) From c8451d142939a5b78a5ee7642768e955654d3a30 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 15:26:57 +0200 Subject: [PATCH 063/446] Fix copy Not sure if it shouldn't be deepcopy, but that doesn't work as something cannot be pickled. --- .../plugins/publish/create_publish_royalrender_job.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index a74c528676..2acd3ba4a5 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -141,8 +141,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): RRJob: RoyalRender publish job. """ - # data = deepcopy(instance.data) - data = instance.data + data = instance.data.copy() subset = data["subset"] job_name = "Publish - {subset}".format(subset=subset) From ac02eac861838435b1a752af89de551ebf534316 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 15:32:48 +0200 Subject: [PATCH 064/446] Small fixes --- .../royalrender/plugins/publish/create_nuke_deadline_job.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py index 0472f2ea80..4a2821e195 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py @@ -142,7 +142,7 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): if not self._instance.data.get("rrJobs"): self._instance.data["rrJobs"] = [] - self._instance.data["rrJobs"] += self.create_jobs() + self._instance.data["rrJobs"].extend(self.create_jobs()) # redefinition of families if "render" in self._instance.data["family"]: @@ -155,7 +155,6 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): self._instance.data["outputDir"] = os.path.dirname( self._instance.data["path"]).replace("\\", "/") - def create_jobs(self): submit_frame_start = int(self._instance.data["frameStartHandle"]) submit_frame_end = int(self._instance.data["frameEndHandle"]) @@ -256,6 +255,8 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): CustomAttributes=custom_attributes ) + return job + @staticmethod def _resolve_rr_path(context, rr_path_name): # type: (Context, str) -> str From 4e455ead316fbc334502be51bde30e5eca0f9e3c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 May 2023 13:37:55 +0200 Subject: [PATCH 065/446] Instance data might not contain "publish" key In that case publish is implicit, more likely it will be set explicitly to False than not existing at all. --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 58d9fd8a05..cf52c113c3 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -804,7 +804,7 @@ def get_published_workfile_instance(context): # test if there is instance of workfile waiting # to be published. - if i.data["publish"] is not True: + if not i.data.get("publish", True): continue return i From 8858c0c0dab5622be8816a169d785a7227e266b6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 May 2023 14:09:13 +0200 Subject: [PATCH 066/446] Get proper rrPathName Instance could be picked non-deterministically, without rrPathName value. --- .../plugins/publish/submit_jobs_to_royalrender.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py index 8546554372..29d5fe6d72 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -34,6 +34,7 @@ class SubmitJobsToRoyalRender(ContextPlugin): # iterate over all instances and try to find RRJobs jobs = [] + instance_rr_path = None for instance in context: if isinstance(instance.data.get("rrJob"), RRJob): jobs.append(instance.data.get("rrJob")) @@ -42,10 +43,11 @@ class SubmitJobsToRoyalRender(ContextPlugin): isinstance(job, RRJob) for job in instance.data.get("rrJobs")): jobs += instance.data.get("rrJobs") + if instance.data.get("rrPathName"): + instance_rr_path = instance.data["rrPathName"] if jobs: - self._rr_root = self._resolve_rr_path( - context, instance.data.get("rrPathName")) # noqa + self._rr_root = self._resolve_rr_path(context, instance_rr_path) if not self._rr_root: raise KnownPublishError( ("Missing RoyalRender root. " From d8e8e10806d7428aefd7f808f53c4f3ca7449f64 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 May 2023 16:38:40 +0200 Subject: [PATCH 067/446] Remove unwanted logging code --- .../plugins/publish/create_nuke_deadline_job.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py index 4a2821e195..8049398845 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py @@ -87,17 +87,6 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): self.rr_api = None def process(self, instance): - # import json - # def _default_json(value): - # return str(value) - # filepath = "C:\\Users\\petrk\\PycharmProjects\\Pype3.0\\pype\\tests\\unit\\openpype\\modules\\royalrender\\plugins\\publish\\resources\\instance.json" - # with open(filepath, "w") as f: - # f.write(json.dumps(instance.data, indent=4, default=_default_json)) - # - # filepath = "C:\\Users\\petrk\\PycharmProjects\\Pype3.0\\pype\\tests\\unit\\openpype\\modules\\royalrender\\plugins\\publish\\resources\\context.json" - # with open(filepath, "w") as f: - # f.write(json.dumps(instance.context.data, indent=4, default=_default_json)) - if not instance.data.get("farm"): self.log.info("Skipping local instance.") return From 45894da8dcecc49d06c4c582f52dcbcf4b9190a4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 May 2023 18:46:10 +0200 Subject: [PATCH 068/446] Fix frame placeholder Without it RR won't find rendered files. --- .../plugins/publish/create_nuke_deadline_job.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py index 8049398845..699f665468 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py @@ -16,7 +16,8 @@ from openpype.modules.royalrender.rr_job import ( from openpype.lib import ( is_running_from_build, BoolDef, - NumberDef + NumberDef, + collect_frames ) from openpype.pipeline import OpenPypePyblishPluginMixin from openpype.pipeline.farm.tools import iter_expected_files @@ -219,6 +220,10 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): self._instance.data["expectedFiles"].extend(expected_files) first_file = next(iter_expected_files(expected_files)) + file_name, file_ext = os.path.splitext(os.path.basename(first_file)) + frame_pattern = ".{}".format(start_frame) + file_name = file_name.replace(frame_pattern, '.#') + job = RRJob( Software="Nuke", Renderer="", @@ -230,9 +235,9 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): SceneName=script_path, IsActive=True, ImageDir=render_dir.replace("\\", "/"), - ImageFilename="{}.".format(os.path.splitext(first_file)[0]), - ImageExtension=os.path.splitext(first_file)[1], - ImagePreNumberLetter=".", + ImageFilename="{}".format(file_name), + ImageExtension=file_ext, + ImagePreNumberLetter="", ImageSingleOutputFile=False, SceneOS=get_rr_platform(), Layer=node_name, From 97043fe3107c6c8d15982b80563d6466af6bbfc7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 May 2023 18:47:16 +0200 Subject: [PATCH 069/446] Use absolute path instead of rootless Rootless path will result jobs won't show up in rrControl. --- .../plugins/publish/create_publish_royalrender_job.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 2acd3ba4a5..29e467e46a 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -66,7 +66,6 @@ class CreatePublishRoyalRenderJob(InstancePlugin): priority = 50 def process(self, instance): - # data = instance.data.copy() context = instance.context self.context = context self.anatomy = instance.context.data["anatomy"] @@ -150,7 +149,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): # Transfer the environment from the original job to this dependent # job, so they use the same environment - metadata_path, roothless_metadata_path = \ + metadata_path, rootless_metadata_path = \ create_metadata_path(instance, self.anatomy) anatomy_data = instance.context.data["anatomyData"] @@ -194,10 +193,13 @@ class CreatePublishRoyalRenderJob(InstancePlugin): priority = self.priority or instance.data.get("priority", 50) + ## rr requires absolut path or all jobs won't show up in rControl + abs_metadata_path = self.anatomy.fill_root(rootless_metadata_path) + args = [ "--headless", 'publish', - roothless_metadata_path, + abs_metadata_path, "--targets", "deadline", "--targets", "farm" ] @@ -212,7 +214,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): SeqFileOffset=0, Version=os.environ.get("OPENPYPE_VERSION"), # executable - SceneName=roothless_metadata_path, + SceneName=abs_metadata_path, # command line arguments CustomAddCmdFlags=" ".join(args), IsActive=True, From 07139db41c002bf686d44c2951e99a43bbe5ee7f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 11:37:58 +0200 Subject: [PATCH 070/446] Implemented waiting on job id --- .../plugins/publish/submit_jobs_to_royalrender.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py index 29d5fe6d72..689fe098d9 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -61,6 +61,14 @@ class SubmitJobsToRoyalRender(ContextPlugin): def process_submission(self, jobs): # type: ([RRJob]) -> None + + idx_pre_id = 0 + for job in jobs: + job.PreID = idx_pre_id + if idx_pre_id > 0: + job.WaitForPreIDs.append(idx_pre_id - 1) + idx_pre_id += 1 + submission = rrApi.create_submission( jobs, self._submission_parameters) From ddf0b3b3ec9a7c63813f3b0bd12d42df72d7fb33 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 11:38:52 +0200 Subject: [PATCH 071/446] Added RequiredMemory Without it default (4GB) is used. --- .../plugins/publish/submit_jobs_to_royalrender.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py index 689fe098d9..6e91dff6ac 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -4,7 +4,11 @@ import tempfile import platform from pyblish.api import IntegratorOrder, ContextPlugin, Context -from openpype.modules.royalrender.api import RRJob, Api as rrApi +from openpype.modules.royalrender.api import ( + RRJob, + Api as rrApi, + SubmitterParameter +) from openpype.pipeline.publish import KnownPublishError @@ -95,7 +99,7 @@ class SubmitJobsToRoyalRender(ContextPlugin): return temp.name def get_submission_parameters(self): - return [] + return [SubmitterParameter("RequiredMemory", "0")] @staticmethod def _resolve_rr_path(context, rr_path_name): From 449157fd73002fc2e4c14ebbde3adf9b84319074 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 16:37:22 +0200 Subject: [PATCH 072/446] Fix executable placeholder --- .../rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg b/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg index 6414ae45a8..864eeaf15a 100644 --- a/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg +++ b/openpype/modules/royalrender/rr_root/render_apps/_config/E01__OpenPype__PublishJob.cfg @@ -30,7 +30,7 @@ CommandLine= CommandLine= -CommandLine= "" --headless publish +CommandLine= "" --headless publish --targets royalrender --targets farm From edcc1cc5f34cf366ff221742d743e89a71ba6368 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 16:39:17 +0200 Subject: [PATCH 073/446] Fix content of metadata file Must contain metadata useful for publish not only properties of RR job. --- .../publish/create_publish_royalrender_job.py | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 29e467e46a..db7118103d 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -13,8 +13,9 @@ from openpype.modules.royalrender.rr_job import ( get_rr_platform ) from openpype.pipeline.publish import KnownPublishError -from openpype.lib.openpype_version import ( - get_OpenPypeVersion, get_openpype_version) +from openpype.pipeline import ( + legacy_io, +) from openpype.pipeline.farm.pyblish_functions import ( create_skeleton_instance, create_instances_for_aov, @@ -114,16 +115,31 @@ class CreatePublishRoyalRenderJob(InstancePlugin): raise KnownPublishError( "Can't create publish job without prior ppducing jobs first") - publish_job = self.get_job(instance, instances) + rr_job = self.get_job(instance, instances) + instance.data["rrJobs"].append(rr_job) - instance.data["rrJobs"].append(publish_job) + # publish job file + publish_job = { + "asset": instance_skeleton_data["asset"], + "frameStart": instance_skeleton_data["frameStart"], + "frameEnd": instance_skeleton_data["frameEnd"], + "fps": instance_skeleton_data["fps"], + "source": instance_skeleton_data["source"], + "user": instance.context.data["user"], + "version": instance.context.data["version"], # this is workfile version + "intent": instance.context.data.get("intent"), + "comment": instance.context.data.get("comment"), + "job": attr.asdict(rr_job), + "session": legacy_io.Session.copy(), + "instances": instances + } metadata_path, rootless_metadata_path = \ create_metadata_path(instance, self.anatomy) self.log.info("Writing json file: {}".format(metadata_path)) with open(metadata_path, "w") as f: - json.dump(attr.asdict(publish_job), f, indent=4, sort_keys=True) + json.dump(publish_job, f, indent=4, sort_keys=True) def get_job(self, instance, instances): """Create RR publishing job. From 0ad2e216d4582901a14954234d67766956b05fea Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 16:39:58 +0200 Subject: [PATCH 074/446] Add logging to log file --- .../plugins/publish/create_publish_royalrender_job.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index db7118103d..28d6faecc0 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -212,12 +212,12 @@ class CreatePublishRoyalRenderJob(InstancePlugin): ## rr requires absolut path or all jobs won't show up in rControl abs_metadata_path = self.anatomy.fill_root(rootless_metadata_path) + # command line set in E01__OpenPype__PublishJob.cfg, here only + # additional logging args = [ - "--headless", - 'publish', - abs_metadata_path, - "--targets", "deadline", - "--targets", "farm" + ">", os.path.join(os.path.dirname(abs_metadata_path), + "rr_out.log"), + "2>&1" ] job = RRJob( From a10bf73c725fdd3ef3ff718f454227981dfa422a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 16:40:28 +0200 Subject: [PATCH 075/446] Fix batching of publish job --- .../plugins/publish/create_publish_royalrender_job.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 28d6faecc0..101234f9f2 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -240,7 +240,8 @@ class CreatePublishRoyalRenderJob(InstancePlugin): ImagePreNumberLetter="", SceneOS=get_rr_platform(), rrEnvList=environment.serialize(), - Priority=priority + Priority=priority, + CompanyProjectName=instance.context.data["projectName"] ) # add assembly jobs as dependencies From 0a6fd30d03a9de544848ec08a24b05b30bd0d24e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 16:40:57 +0200 Subject: [PATCH 076/446] Remove unneeded import --- .../plugins/publish/create_publish_royalrender_job.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 101234f9f2..4ce96a908d 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -2,7 +2,6 @@ """Create publishing job on RoyalRender.""" import os import attr -from copy import deepcopy import json from pyblish.api import InstancePlugin, IntegratorOrder, Instance From b00126677550ce85e02d794af30715d81e5d4fb7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 17:14:25 +0200 Subject: [PATCH 077/446] Clean up --- .../publish/create_publish_royalrender_job.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 4ce96a908d..e6836ad4e7 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -25,6 +25,14 @@ from openpype.pipeline.farm.pyblish_functions import ( class CreatePublishRoyalRenderJob(InstancePlugin): + """Creates job which publishes rendered files to publish area. + + Job waits until all rendering jobs are finished, triggers `publish` command + where it reads from prepared .json file with metadata about what should + be published, renames prepared images and publishes them. + + When triggered it produces .log file next to .json file in work area. + """ label = "Create publish job in RR" order = IntegratorOrder + 0.2 icon = "tractor" @@ -62,6 +70,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "AVALON_APP_NAME", "OPENPYPE_USERNAME", "OPENPYPE_SG_USER", + "OPENPYPE_MONGO" ] priority = 50 @@ -112,7 +121,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): self.log.error(("There is no prior RoyalRender " "job on the instance.")) raise KnownPublishError( - "Can't create publish job without prior ppducing jobs first") + "Can't create publish job without prior rendering jobs first") rr_job = self.get_job(instance, instances) instance.data["rrJobs"].append(rr_job) @@ -125,7 +134,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "fps": instance_skeleton_data["fps"], "source": instance_skeleton_data["source"], "user": instance.context.data["user"], - "version": instance.context.data["version"], # this is workfile version + "version": instance.context.data["version"], # workfile version "intent": instance.context.data.get("intent"), "comment": instance.context.data.get("comment"), "job": attr.asdict(rr_job), @@ -157,7 +166,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): """ data = instance.data.copy() subset = data["subset"] - job_name = "Publish - {subset}".format(subset=subset) + jobname = "Publish - {subset}".format(subset=subset) instance_version = instance.data.get("version") # take this if exists override_version = instance_version if instance_version != 1 else None @@ -173,11 +182,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): "AVALON_PROJECT": anatomy_data["project"]["name"], "AVALON_ASSET": anatomy_data["asset"], "AVALON_TASK": anatomy_data["task"]["name"], - "OPENPYPE_USERNAME": anatomy_data["user"], - "OPENPYPE_PUBLISH_JOB": "1", - "OPENPYPE_RENDER_JOB": "0", - "OPENPYPE_REMOTE_JOB": "0", - "OPENPYPE_LOG_NO_COLORS": "1" + "OPENPYPE_USERNAME": anatomy_data["user"] }) # add environments from self.environ_keys @@ -200,12 +205,6 @@ class CreatePublishRoyalRenderJob(InstancePlugin): if job_environ.get(env_j_key): environment[env_j_key] = job_environ[env_j_key] - # Add mongo url if it's enabled - if instance.context.data.get("deadlinePassMongoUrl"): - mongo_url = os.environ.get("OPENPYPE_MONGO") - if mongo_url: - environment["OPENPYPE_MONGO"] = mongo_url - priority = self.priority or instance.data.get("priority", 50) ## rr requires absolut path or all jobs won't show up in rControl @@ -222,13 +221,11 @@ class CreatePublishRoyalRenderJob(InstancePlugin): job = RRJob( Software="OpenPype", Renderer="Once", - # path to OpenPype SeqStart=1, SeqEnd=1, SeqStep=1, SeqFileOffset=0, Version=os.environ.get("OPENPYPE_VERSION"), - # executable SceneName=abs_metadata_path, # command line arguments CustomAddCmdFlags=" ".join(args), @@ -240,6 +237,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): SceneOS=get_rr_platform(), rrEnvList=environment.serialize(), Priority=priority, + CustomSHotName=jobname, CompanyProjectName=instance.context.data["projectName"] ) From 540981da6ab257c5a6cfd16ae723c6a94520c938 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 17:19:33 +0200 Subject: [PATCH 078/446] Clean up --- .../plugins/publish/create_nuke_deadline_job.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py index 699f665468..8d05373e32 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Submitting render job to RoyalRender.""" -import copy import os import re import platform @@ -17,13 +16,12 @@ from openpype.lib import ( is_running_from_build, BoolDef, NumberDef, - collect_frames ) from openpype.pipeline import OpenPypePyblishPluginMixin -from openpype.pipeline.farm.tools import iter_expected_files class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): + """Creates separate rendering job for Royal Render""" label = "Create Nuke Render job in RR" order = IntegratorOrder + 0.1 hosts = ["nuke"] @@ -202,6 +200,7 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): batch_name += datetime.now().strftime("%d%m%Y%H%M%S") output_filename_0 = self.preview_fname(render_path) + _, file_ext = os.path.splitext(os.path.basename(render_path)) custom_attributes = [] if is_running_from_build(): @@ -218,11 +217,6 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): expected_files = self.expected_files( render_path, start_frame, end_frame) self._instance.data["expectedFiles"].extend(expected_files) - first_file = next(iter_expected_files(expected_files)) - - file_name, file_ext = os.path.splitext(os.path.basename(first_file)) - frame_pattern = ".{}".format(start_frame) - file_name = file_name.replace(frame_pattern, '.#') job = RRJob( Software="Nuke", @@ -235,14 +229,14 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): SceneName=script_path, IsActive=True, ImageDir=render_dir.replace("\\", "/"), - ImageFilename="{}".format(file_name), + ImageFilename="{}".format(output_filename_0), ImageExtension=file_ext, ImagePreNumberLetter="", ImageSingleOutputFile=False, SceneOS=get_rr_platform(), Layer=node_name, SceneDatabaseDir=script_path, - CustomSHotName=self._instance.context.data["asset"], + CustomSHotName=jobname, CompanyProjectName=self._instance.context.data["projectName"], ImageWidth=self._instance.data["resolutionWidth"], ImageHeight=self._instance.data["resolutionHeight"], From ddb227bd959991cae8a51faa04e9aee28af7e381 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 17:25:13 +0200 Subject: [PATCH 079/446] Remove unfinished file --- .../default_modules/royal_render/test_rr_job.py | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 tests/unit/openpype/default_modules/royal_render/test_rr_job.py diff --git a/tests/unit/openpype/default_modules/royal_render/test_rr_job.py b/tests/unit/openpype/default_modules/royal_render/test_rr_job.py deleted file mode 100644 index ab8b1bfd50..0000000000 --- a/tests/unit/openpype/default_modules/royal_render/test_rr_job.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -"""Test suite for User Settings.""" -# import pytest -# from openpype.modules import ModulesManager - - -def test_rr_job(): - # manager = ModulesManager() - # rr_module = manager.modules_by_name["royalrender"] - ... From f3ce4c6b71b3f0aec014b78452ee6f489023c4a1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 18:33:13 +0200 Subject: [PATCH 080/446] Hound --- .../deadline/plugins/publish/submit_publish_job.py | 11 ++++------- .../plugins/publish/create_nuke_deadline_job.py | 5 +++-- .../plugins/publish/create_publish_royalrender_job.py | 7 ++----- openpype/modules/royalrender/rr_job.py | 2 +- openpype/pipeline/farm/pyblish_functions.py | 7 ++++--- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index ff6bcf1801..be0863f9b2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -336,12 +336,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): return instance_skeleton_data = create_skeleton_instance( - instance, - families_transfer=self.families_transfer, - instance_transfer=self.instance_transfer) - - instances = None - + instance, + families_transfer=self.families_transfer, + instance_transfer=self.instance_transfer) """ if content of `expectedFiles` list are dictionaries, we will handle it as list of AOVs, creating instance for every one of them. @@ -481,7 +478,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "fps": instance_skeleton_data["fps"], "source": instance_skeleton_data["source"], "user": instance.context.data["user"], - "version": instance.context.data["version"], # this is workfile version + "version": instance.context.data["version"], # workfile version "intent": instance.context.data.get("intent"), "comment": instance.context.data.get("comment"), "job": render_job or None, diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py index 8d05373e32..0d5e440c90 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py @@ -200,7 +200,8 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): batch_name += datetime.now().strftime("%d%m%Y%H%M%S") output_filename_0 = self.preview_fname(render_path) - _, file_ext = os.path.splitext(os.path.basename(render_path)) + file_name, file_ext = os.path.splitext( + os.path.basename(output_filename_0)) custom_attributes = [] if is_running_from_build(): @@ -229,7 +230,7 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): SceneName=script_path, IsActive=True, ImageDir=render_dir.replace("\\", "/"), - ImageFilename="{}".format(output_filename_0), + ImageFilename=file_name, ImageExtension=file_ext, ImagePreNumberLetter="", ImageSingleOutputFile=False, diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index e6836ad4e7..3cc63db377 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -4,7 +4,7 @@ import os import attr import json -from pyblish.api import InstancePlugin, IntegratorOrder, Instance +from pyblish.api import InstancePlugin, IntegratorOrder from openpype.modules.royalrender.rr_job import ( RRJob, @@ -168,9 +168,6 @@ class CreatePublishRoyalRenderJob(InstancePlugin): subset = data["subset"] jobname = "Publish - {subset}".format(subset=subset) - instance_version = instance.data.get("version") # take this if exists - override_version = instance_version if instance_version != 1 else None - # Transfer the environment from the original job to this dependent # job, so they use the same environment metadata_path, rootless_metadata_path = \ @@ -207,7 +204,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): priority = self.priority or instance.data.get("priority", 50) - ## rr requires absolut path or all jobs won't show up in rControl + # rr requires absolut path or all jobs won't show up in rControl abs_metadata_path = self.anatomy.fill_root(rootless_metadata_path) # command line set in E01__OpenPype__PublishJob.cfg, here only diff --git a/openpype/modules/royalrender/rr_job.py b/openpype/modules/royalrender/rr_job.py index 8d96b8ff4a..b85ac592f8 100644 --- a/openpype/modules/royalrender/rr_job.py +++ b/openpype/modules/royalrender/rr_job.py @@ -32,7 +32,7 @@ class RREnvList(dict): """Parse rrEnvList string and return it as RREnvList object.""" out = RREnvList() for var in data.split("~~~"): - k, v = data.split("=") + k, v = var.split("=") out[k] = v return out diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 6c08545b1b..dbba9f8a9a 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -92,7 +92,6 @@ def extend_frames(asset, subset, start, end): updated_start = min(start, prev_start) updated_end = max(end, prev_end) - return updated_start, updated_end @@ -217,9 +216,11 @@ def create_skeleton_instance( log = Logger.get_logger("farm_publishing") log.warning(("Could not find root path for remapping \"{}\". " "This may cause issues.").format(source)) - + family = ("render" + if "prerender" not in instance.data["families"] + else "prerender") instance_skeleton_data = { - "family": "render" if "prerender" not in instance.data["families"] else "prerender", # noqa: E401 + "family": family, "subset": data["subset"], "families": families, "asset": data["asset"], From 36ef2b5b5f8712764bc2e2219d452838fafdc999 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 May 2023 18:39:40 +0200 Subject: [PATCH 081/446] Hound --- .../deadline/plugins/publish/submit_publish_job.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index be0863f9b2..2b5916cea5 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -336,9 +336,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): return instance_skeleton_data = create_skeleton_instance( - instance, - families_transfer=self.families_transfer, - instance_transfer=self.instance_transfer) + instance, families_transfer=self.families_transfer, + instance_transfer=self.instance_transfer) """ if content of `expectedFiles` list are dictionaries, we will handle it as list of AOVs, creating instance for every one of them. @@ -447,8 +446,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): render_job["Props"]["Batch"] = instance.data.get( "jobBatchName") else: - render_job["Props"]["Batch"] = os.path.splitext( - os.path.basename(instance.context.data.get("currentFile")))[0] + batch = os.path.splitext(os.path.basename( + instance.context.data.get("currentFile")))[0] + render_job["Props"]["Batch"] = batch # User is deadline user render_job["Props"]["User"] = instance.context.data.get( "deadlineUser", getpass.getuser()) From b654b10e049a9a18860c431568d9cd645f60daf7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 May 2023 11:10:42 +0200 Subject: [PATCH 082/446] Handle conflict with recent develop Added explicit disabling of adding review. --- .../plugins/publish/submit_publish_job.py | 13 ++++++---- .../publish/create_publish_royalrender_job.py | 11 +++++++-- openpype/pipeline/farm/pyblish_functions.py | 24 +++++++++++++------ 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 71a609997f..acd47e3224 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -299,7 +299,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): deadline_publish_job_id = response.json()["_id"] return deadline_publish_job_id - + def _solve_families(self, instance, preview=False): families = instance.get("families") @@ -379,19 +379,24 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): This will result in one instance with two representations: `foo` and `xxx` """ - + do_not_add_review = False + if instance.data.get("review") is False: + self.log.debug("Instance has review explicitly disabled.") + do_not_add_review = True if isinstance(instance.data.get("expectedFiles")[0], dict): instances = create_instances_for_aov( instance, instance_skeleton_data, - self.aov_filter, self.skip_integration_repre_list) + self.aov_filter, self.skip_integration_repre_list, + do_not_add_review) else: representations = prepare_representations( instance_skeleton_data, instance.data.get("expectedFiles"), self.anatomy, self.aov_filter, - self.skip_integration_repre_list + self.skip_integration_repre_list, + do_not_add_review ) if "representations" not in instance_skeleton_data.keys(): diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 3cc63db377..af5bdfd5e6 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -88,10 +88,16 @@ class CreatePublishRoyalRenderJob(InstancePlugin): families_transfer=self.families_transfer, instance_transfer=self.instance_transfer) + do_not_add_review = False + if instance.data.get("review") is False: + self.log.debug("Instance has review explicitly disabled.") + do_not_add_review = True + if isinstance(instance.data.get("expectedFiles")[0], dict): instances = create_instances_for_aov( instance, instance_skeleton_data, - self.aov_filter, self.skip_integration_repre_list) + self.aov_filter, self.skip_integration_repre_list, + do_not_add_review) else: representations = prepare_representations( @@ -99,7 +105,8 @@ class CreatePublishRoyalRenderJob(InstancePlugin): instance.data.get("expectedFiles"), self.anatomy, self.aov_filter, - self.skip_integration_repre_list + self.skip_integration_repre_list, + do_not_add_review ) if "representations" not in instance_skeleton_data.keys(): diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index dbba9f8a9a..be031b1fde 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -286,7 +286,8 @@ def _solve_families(families): def prepare_representations(instance, exp_files, anatomy, aov_filter, - skip_integration_repre_list): + skip_integration_repre_list, + do_not_add_review): """Create representations for file sequences. This will return representations of expected files if they are not @@ -299,7 +300,8 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, exp_files (list): list of expected files anatomy (Anatomy): aov_filter (dict): add review for specific aov names - skip_integration_repre_list (list): exclude specific extensions + skip_integration_repre_list (list): exclude specific extensions, + do_not_add_review (bool): explicitly skip review Returns: list of representations @@ -351,6 +353,7 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, if instance.get("slate"): frame_start -= 1 + preview = preview and not do_not_add_review rep = { "name": ext, "ext": ext, @@ -406,6 +409,7 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, preview = match_aov_pattern( host_name, aov_filter, remainder ) + preview = preview and not do_not_add_review if preview: rep.update({ "fps": instance.get("fps"), @@ -428,7 +432,8 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, def create_instances_for_aov(instance, skeleton, aov_filter, - skip_integration_repre_list): + skip_integration_repre_list, + do_not_add_review): """Create instances from AOVs. This will create new pyblish.api.Instances by going over expected @@ -437,6 +442,7 @@ def create_instances_for_aov(instance, skeleton, aov_filter, Args: instance (pyblish.api.Instance): Original instance. skeleton (dict): Skeleton instance data. + skip_integration_repre_list (list): skip Returns: list of pyblish.api.Instance: Instances created from @@ -481,12 +487,13 @@ def create_instances_for_aov(instance, skeleton, aov_filter, skeleton, aov_filter, additional_color_data, - skip_integration_repre_list + skip_integration_repre_list, + do_not_add_review ) def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, - skip_integration_repre_list): + skip_integration_repre_list, do_not_add_review): """Create instance for each AOV found. This will create new instance for every AOV it can detect in expected @@ -496,7 +503,10 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, instance (pyblish.api.Instance): Original instance. skeleton (dict): Skeleton data for instance (those needed) later by collector. - additional_data (dict): ... + additional_data (dict): .. + skip_integration_repre_list (list): list of extensions that shouldn't + be published + do_not_addbe _review (bool): explicitly disable review Returns: @@ -631,7 +641,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, if ext in skip_integration_repre_list: rep["tags"].append("delete") - if preview: + if preview and not do_not_add_review: new_instance["families"] = _solve_families(new_instance) new_instance["representations"] = [rep] From c302b61d729826a223651f0fa7bc36992601030c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 May 2023 11:11:22 +0200 Subject: [PATCH 083/446] Removed _extend_frames Moved to publish_functions --- .../plugins/publish/submit_publish_job.py | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index acd47e3224..853a3983e7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -515,47 +515,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): with open(metadata_path, "w") as f: json.dump(publish_job, f, indent=4, sort_keys=True) - def _extend_frames(self, asset, subset, start, end): - """Get latest version of asset nad update frame range. - - Based on minimum and maximuma values. - - Arguments: - asset (str): asset name - subset (str): subset name - start (int): start frame - end (int): end frame - - Returns: - (int, int): upddate frame start/end - - """ - # Frame comparison - prev_start = None - prev_end = None - - project_name = legacy_io.active_project() - version = get_last_version_by_subset_name( - project_name, - subset, - asset_name=asset - ) - - # Set prev start / end frames for comparison - if not prev_start and not prev_end: - prev_start = version["data"]["frameStart"] - prev_end = version["data"]["frameEnd"] - - updated_start = min(start, prev_start) - updated_end = max(end, prev_end) - - self.log.info( - "Updating start / end frame : " - "{} - {}".format(updated_start, updated_end) - ) - - return updated_start, updated_end - def _get_publish_folder(self, anatomy, template_data, asset, subset, family='render', version=None): From 1aef4c57f3d5d0f3fd70035ff3b6f452b43aecbc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 May 2023 12:23:48 +0200 Subject: [PATCH 084/446] Fix missing import --- openpype/modules/deadline/abstract_submit_deadline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index f6696b2c05..c2cc518e40 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -21,7 +21,10 @@ from openpype.pipeline.publish import ( AbstractMetaInstancePlugin, KnownPublishError ) -from openpype.pipeline.publish.lib import replace_published_scene +from openpype.pipeline.publish.lib import ( + replace_published_scene, + get_published_workfile_instance +) JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) From 78f9a68b35027daa1218491240be90f117b2486c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 May 2023 13:10:02 +0200 Subject: [PATCH 085/446] Fix aov handling Use col.head directly as rem in second logic path has no .head, it is only str. Fix review handling --- openpype/pipeline/farm/pyblish_functions.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index be031b1fde..7444c22198 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -353,6 +353,7 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, if instance.get("slate"): frame_start -= 1 + # explicitly disable review by user preview = preview and not do_not_add_review rep = { "name": ext, @@ -544,14 +545,14 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, if len(cols) != 1: raise ValueError("Only one image sequence type is expected.") # noqa: E501 ext = cols[0].tail.lstrip(".") - col = list(cols[0]) + col = cols[0].head # create subset name `familyTaskSubset_AOV` group_name = 'render{}{}{}{}'.format( task[0].upper(), task[1:], subset[0].upper(), subset[1:]) - cam = [c for c in cameras if c in col.head] + cam = [c for c in cameras if c in col] if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) @@ -577,8 +578,6 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, app = os.environ.get("AVALON_APP", "") - preview = False - if isinstance(col, list): render_file_name = os.path.basename(col[0]) else: @@ -587,11 +586,13 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, preview = match_aov_pattern(app, aov_patterns, render_file_name) # toggle preview on if multipart is on - if instance.data.get("multipartExr"): log.debug("Adding preview tag because its multipartExr") preview = True + # explicitly disable review by user + preview = preview and not do_not_add_review + new_instance = deepcopy(skeleton) new_instance["subsetGroup"] = group_name if preview: @@ -641,7 +642,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, if ext in skip_integration_repre_list: rep["tags"].append("delete") - if preview and not do_not_add_review: + if preview: new_instance["families"] = _solve_families(new_instance) new_instance["representations"] = [rep] From a4434d932b5462d5e4255152c27d3e233466467d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 May 2023 14:54:25 +0200 Subject: [PATCH 086/446] Fix adding review to families Renamed function to (bit) more reasonable name. --- openpype/pipeline/farm/pyblish_functions.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 7444c22198..09cb50941a 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -269,8 +269,11 @@ def create_skeleton_instance( return instance_skeleton_data -def _solve_families(families): - """Solve families. +def _add_review_families(families): + """Adds review flag to families. + + Handles situation when new instances are created which should have review + in families. In that case they should have 'ftrack' too. TODO: This is ugly and needs to be refactored. Ftrack family should be added in different way (based on if the module is enabled?) @@ -382,7 +385,7 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, representations.append(rep) if preview: - instance["families"] = _solve_families(instance["families"]) + instance["families"] = _add_review_families(instance["families"]) # add remainders as representations for remainder in remainders: @@ -416,7 +419,7 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, "fps": instance.get("fps"), "tags": ["review"] }) - instance["families"] = _solve_families(instance["families"]) + instance["families"] = _add_review_families(instance["families"]) already_there = False for repre in instance.get("representations", []): @@ -643,7 +646,8 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, rep["tags"].append("delete") if preview: - new_instance["families"] = _solve_families(new_instance) + new_instance["families"] = _add_review_families( + new_instance["families"]) new_instance["representations"] = [rep] From d745ebce16a2d64a5b3739c3aaa795d69726379b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 May 2023 14:54:50 +0200 Subject: [PATCH 087/446] Fix missing colorspaceTemplate key --- openpype/pipeline/farm/pyblish_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 09cb50941a..0b3e4acaae 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -472,6 +472,7 @@ def create_instances_for_aov(instance, skeleton, aov_filter, colorspace_template, anatomy) except ValueError as e: log.warning(e) + additional_color_data["colorspaceTemplate"] = colorspace_template # if there are subset to attach to and more than one AOV, # we cannot proceed. From 162b58ce7a574d8d9ea5efde58c01e2ec9c99a29 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 May 2023 15:00:58 +0200 Subject: [PATCH 088/446] Fix access to anatomy --- .../plugins/publish/submit_publish_job.py | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 853a3983e7..4f12552d34 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -165,6 +165,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): subset = data["subset"] job_name = "Publish - {subset}".format(subset=subset) + anatomy = instance.context.data['anatomy'] + # instance.data.get("subset") != instances[0]["subset"] # 'Main' vs 'renderMain' override_version = None @@ -172,7 +174,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if instance_version != 1: override_version = instance_version output_dir = self._get_publish_folder( - instance.context.data['anatomy'], + anatomy, deepcopy(instance.data["anatomyData"]), instance.data.get("asset"), instances[0]["subset"], @@ -183,7 +185,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Transfer the environment from the original job to this dependent # job so they use the same environment metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, self.anatomy) + create_metadata_path(instance, anatomy) environment = { "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], @@ -263,13 +265,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Adding tile assembly jobs as dependencies...") job_index = 0 for assembly_id in instance.data.get("assemblySubmissionJobs"): - payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 + payload["JobInfo"]["JobDependency{}".format( + job_index)] = assembly_id # noqa: E501 job_index += 1 elif instance.data.get("bakingSubmissionJobs"): self.log.info("Adding baking submission jobs as dependencies...") job_index = 0 for assembly_id in instance.data["bakingSubmissionJobs"]: - payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 + payload["JobInfo"]["JobDependency{}".format( + job_index)] = assembly_id # noqa: E501 job_index += 1 else: payload["JobInfo"]["JobDependency0"] = job["_id"] @@ -300,25 +304,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): return deadline_publish_job_id - def _solve_families(self, instance, preview=False): - families = instance.get("families") - - # if we have one representation with preview tag - # flag whole instance for review and for ftrack - if preview: - if "ftrack" not in families: - if os.environ.get("FTRACK_SERVER"): - self.log.debug( - "Adding \"ftrack\" to families because of preview tag." - ) - families.append("ftrack") - if "review" not in families: - self.log.debug( - "Adding \"review\" to families because of preview tag." - ) - families.append("review") - instance["families"] = families - def process(self, instance): # type: (pyblish.api.Instance) -> None """Process plugin. @@ -335,6 +320,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Skipping local instance.") return + anatomy = instance.context.data["anatomy"] + instance_skeleton_data = create_skeleton_instance( instance, families_transfer=self.families_transfer, instance_transfer=self.instance_transfer) @@ -393,7 +380,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): representations = prepare_representations( instance_skeleton_data, instance.data.get("expectedFiles"), - self.anatomy, + anatomy, self.aov_filter, self.skip_integration_repre_list, do_not_add_review @@ -509,9 +496,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): publish_job.update({"ftrack": ftrack}) metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, self.anatomy) + create_metadata_path(instance, anatomy) - self.log.info("Writing json file: {}".format(metadata_path)) with open(metadata_path, "w") as f: json.dump(publish_job, f, indent=4, sort_keys=True) From ad488c7ee4f54a209c617d3a6043122d41109deb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 May 2023 16:50:52 +0200 Subject: [PATCH 089/446] Fix use proper families renderLayer is only for processing, final family is 'render'/'prerender'. --- openpype/pipeline/farm/pyblish_functions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 0b3e4acaae..1bfc5e3dc2 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -188,12 +188,6 @@ def create_skeleton_instance( data = instance.data.copy() anatomy = instance.context.data["anatomy"] # type: Anatomy - families = [data["family"]] - - # pass review to families if marked as review - if data.get("review"): - families.append("review") - # get time related data from instance (or context) time_data = get_time_data_from_instance_or_context(instance) @@ -219,6 +213,12 @@ def create_skeleton_instance( family = ("render" if "prerender" not in instance.data["families"] else "prerender") + families = [family] + + # pass review to families if marked as review + if data.get("review"): + families.append("review") + instance_skeleton_data = { "family": family, "subset": data["subset"], From c1f3201316ab938ec2d97e65ca32a0021818d0f2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 May 2023 16:51:41 +0200 Subject: [PATCH 090/446] Fix use proper subset name Subset name should contain task, not only 'Main' --- openpype/pipeline/farm/pyblish_functions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 1bfc5e3dc2..89bff6dfee 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -210,6 +210,7 @@ def create_skeleton_instance( log = Logger.get_logger("farm_publishing") log.warning(("Could not find root path for remapping \"{}\". " "This may cause issues.").format(source)) + family = ("render" if "prerender" not in instance.data["families"] else "prerender") @@ -594,11 +595,12 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, log.debug("Adding preview tag because its multipartExr") preview = True + new_instance = deepcopy(skeleton) + new_instance["subset"] = subset_name + new_instance["subsetGroup"] = group_name + # explicitly disable review by user preview = preview and not do_not_add_review - - new_instance = deepcopy(skeleton) - new_instance["subsetGroup"] = group_name if preview: new_instance["review"] = True From 6df0fbccf77680b2621ccbea52703a980c8a9548 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 May 2023 15:33:33 +0200 Subject: [PATCH 091/446] OP-1066 - fix changed imports from pyblish.api import breaks Python3 hosts --- openpype/hosts/maya/api/fbx.py | 4 ++-- .../plugins/publish/create_maya_royalrender_job.py | 8 ++++---- .../plugins/publish/create_nuke_deadline_job.py | 9 +++++---- .../plugins/publish/create_publish_royalrender_job.py | 6 +++--- .../plugins/publish/submit_jobs_to_royalrender.py | 8 ++++---- openpype/pipeline/farm/pyblish_functions.py | 4 ++-- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 260241f5fc..e9cf8af491 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -2,7 +2,7 @@ """Tools to work with FBX.""" import logging -from pyblish.api import Instance +import pyblish.api from maya import cmds # noqa import maya.mel as mel # noqa @@ -141,7 +141,7 @@ class FBXExtractor: return options def set_options_from_instance(self, instance): - # type: (Instance) -> None + # type: (pyblish.api.Instance) -> None """Sets FBX export options from data in the instance. Args: diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 8461e74d6d..1bb2785dd6 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -5,7 +5,7 @@ import sys import platform from maya.OpenMaya import MGlobal # noqa -from pyblish.api import InstancePlugin, IntegratorOrder, Context +import pyblish.api from openpype.lib import is_running_from_build from openpype.pipeline.publish.lib import get_published_workfile_instance from openpype.pipeline.publish import KnownPublishError @@ -14,9 +14,9 @@ from openpype.modules.royalrender.rr_job import RRJob, CustomAttribute from openpype.pipeline.farm.tools import iter_expected_files -class CreateMayaRoyalRenderJob(InstancePlugin): +class CreateMayaRoyalRenderJob(pyblish.api.InstancePlugin): label = "Create Maya Render job in RR" - order = IntegratorOrder + 0.1 + order = pyblish.api.IntegratorOrder + 0.1 families = ["renderlayer"] targets = ["local"] use_published = True @@ -128,7 +128,7 @@ class CreateMayaRoyalRenderJob(InstancePlugin): @staticmethod def _resolve_rr_path(context, rr_path_name): - # type: (Context, str) -> str + # type: (pyblish.api.Context, str) -> str rr_settings = ( context.data ["system_settings"] diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py index 0d5e440c90..a90c4c4f83 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py @@ -5,7 +5,7 @@ import re import platform from datetime import datetime -from pyblish.api import InstancePlugin, IntegratorOrder, Context +import pyblish.api from openpype.tests.lib import is_in_tests from openpype.pipeline.publish.lib import get_published_workfile_instance from openpype.pipeline.publish import KnownPublishError @@ -20,10 +20,11 @@ from openpype.lib import ( from openpype.pipeline import OpenPypePyblishPluginMixin -class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): +class CreateNukeRoyalRenderJob(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): """Creates separate rendering job for Royal Render""" label = "Create Nuke Render job in RR" - order = IntegratorOrder + 0.1 + order = pyblish.api.IntegratorOrder + 0.1 hosts = ["nuke"] families = ["render", "prerender"] targets = ["local"] @@ -248,7 +249,7 @@ class CreateNukeRoyalRenderJob(InstancePlugin, OpenPypePyblishPluginMixin): @staticmethod def _resolve_rr_path(context, rr_path_name): - # type: (Context, str) -> str + # type: (pyblish.api.Context, str) -> str rr_settings = ( context.data ["system_settings"] diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index af5bdfd5e6..c652509373 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -4,7 +4,7 @@ import os import attr import json -from pyblish.api import InstancePlugin, IntegratorOrder +import pyblish.api from openpype.modules.royalrender.rr_job import ( RRJob, @@ -24,7 +24,7 @@ from openpype.pipeline.farm.pyblish_functions import ( ) -class CreatePublishRoyalRenderJob(InstancePlugin): +class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin): """Creates job which publishes rendered files to publish area. Job waits until all rendering jobs are finished, triggers `publish` command @@ -34,7 +34,7 @@ class CreatePublishRoyalRenderJob(InstancePlugin): When triggered it produces .log file next to .json file in work area. """ label = "Create publish job in RR" - order = IntegratorOrder + 0.2 + order = pyblish.api.IntegratorOrder + 0.2 icon = "tractor" targets = ["local"] hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"] diff --git a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py index 6e91dff6ac..8fc8604b83 100644 --- a/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py +++ b/openpype/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -3,7 +3,7 @@ import tempfile import platform -from pyblish.api import IntegratorOrder, ContextPlugin, Context +import pyblish.api from openpype.modules.royalrender.api import ( RRJob, Api as rrApi, @@ -12,10 +12,10 @@ from openpype.modules.royalrender.api import ( from openpype.pipeline.publish import KnownPublishError -class SubmitJobsToRoyalRender(ContextPlugin): +class SubmitJobsToRoyalRender(pyblish.api.ContextPlugin): """Find all jobs, create submission XML and submit it to RoyalRender.""" label = "Submit jobs to RoyalRender" - order = IntegratorOrder + 0.3 + order = pyblish.api.IntegratorOrder + 0.3 targets = ["local"] def __init__(self): @@ -103,7 +103,7 @@ class SubmitJobsToRoyalRender(ContextPlugin): @staticmethod def _resolve_rr_path(context, rr_path_name): - # type: (Context, str) -> str + # type: (pyblish.api.Context, str) -> str rr_settings = ( context.data ["system_settings"] diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 89bff6dfee..8be0887b0d 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -1,6 +1,6 @@ import copy import attr -from pyblish.api import Instance +import pyblish.api import os import clique from copy import deepcopy @@ -161,7 +161,7 @@ def get_transferable_representations(instance): def create_skeleton_instance( instance, families_transfer=None, instance_transfer=None): - # type: (Instance, list, dict) -> dict + # type: (pyblish.api.Instance, list, dict) -> dict """Create skeleton instance from original instance data. This will create dictionary containing skeleton From 2c3cd1c630e75292b9b32676ce2fb7638fcdb94d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 May 2023 15:34:32 +0200 Subject: [PATCH 092/446] OP-1066 - renamed file --- ...create_nuke_deadline_job.py => create_nuke_royalrender_job.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/modules/royalrender/plugins/publish/{create_nuke_deadline_job.py => create_nuke_royalrender_job.py} (100%) diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py similarity index 100% rename from openpype/modules/royalrender/plugins/publish/create_nuke_deadline_job.py rename to openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py From 86a1357033c18a82f76385df8ae8531ae446f48e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 May 2023 18:23:38 +0200 Subject: [PATCH 093/446] OP-1066 - sanitize version RR expect version in format 3.157 instead proper 3.15.7-nightly.2 --- .../publish/create_publish_royalrender_job.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index c652509373..6eb8f2649e 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -3,6 +3,7 @@ import os import attr import json +import re import pyblish.api @@ -229,7 +230,7 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin): SeqEnd=1, SeqStep=1, SeqFileOffset=0, - Version=os.environ.get("OPENPYPE_VERSION"), + Version=self._sanitize_version(os.environ.get("OPENPYPE_VERSION")), SceneName=abs_metadata_path, # command line arguments CustomAddCmdFlags=" ".join(args), @@ -256,3 +257,26 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin): job.WaitForPreIDs += jobs_pre_ids return job + + def _sanitize_version(self, version): + """Returns version in format MAJOR.MINORPATCH + + 3.15.7-nightly.2 >> 3.157 + """ + VERSION_REGEX = re.compile( + r"(?P0|[1-9]\d*)" + r"\.(?P0|[1-9]\d*)" + r"\.(?P0|[1-9]\d*)" + r"(?:-(?P[a-zA-Z\d\-.]*))?" + r"(?:\+(?P[a-zA-Z\d\-.]*))?" + ) + + valid_parts = VERSION_REGEX.findall(version) + if len(valid_parts) != 1: + # Return invalid version with filled 'origin' attribute + return version + + # Unpack found version + major, minor, patch, pre, post = valid_parts[0] + + return "{}.{}{}".format(major, minor, patch) From 56f2edfa53a7cf4bde01cc69b8c1ae28514d90bd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 17 May 2023 16:56:59 +0200 Subject: [PATCH 094/446] Add prerender family to collector for RR path --- .../plugins/publish/collect_rr_path_from_instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index e21cd7c39f..ff5040c8df 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -7,7 +7,7 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder label = "Collect Royal Render path name from the Instance" - families = ["render"] + families = ["render", "prerender"] def process(self, instance): instance.data["rrPathName"] = self._collect_rr_path_name(instance) From f6118ed6a8c79c9b6a89890e610771521d0f62e2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 May 2023 17:42:37 +0200 Subject: [PATCH 095/446] Added docuumentation --- website/docs/admin_settings_system.md | 4 +++ website/docs/module_royalrender.md | 37 +++++++++++++++++++++++++++ website/sidebars.js | 1 + 3 files changed, 42 insertions(+) create mode 100644 website/docs/module_royalrender.md diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index d61713ccd5..8abcefd24d 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -102,6 +102,10 @@ workstation that should be submitting render jobs to muster via OpenPype. **`templates mapping`** - you can customize Muster templates to match your existing setup here. +### Royal Render + +**`Royal Render Root Paths`** - multi platform paths to Royal Render installation. + ### Clockify **`Workspace Name`** - name of the clockify workspace where you would like to be sending all the timelogs. diff --git a/website/docs/module_royalrender.md b/website/docs/module_royalrender.md new file mode 100644 index 0000000000..2b75fbefef --- /dev/null +++ b/website/docs/module_royalrender.md @@ -0,0 +1,37 @@ +--- +id: module_royalrender +title: Royal Render Administration +sidebar_label: Royal Render +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +## Preparation + +For [Royal Render](hhttps://www.royalrender.de/) support you need to set a few things up in both OpenPype and Royal Render itself + +1. Deploy OpenPype executable to all nodes of Royal Render farm. See [Install & Run](admin_use.md) + +2. Enable Royal Render Module in the [OpenPype Admin Settings](admin_settings_system.md#royal-render). + +3. Point OpenPype to your Royal Render installation in the [OpenPype Admin Settings](admin_settings_system.md#royal-render). + +4. Install our custom plugin and scripts to your RR repository. It should be as simple as copying content of `openpype/modules/royalrender/rr_root` to `path/to/your/royalrender/repository`. + + +## Configuration + +OpenPype integration for Royal Render consists of pointing RR to location of Openpype executable. That is being done by copying `_install_paths/OpenPype.cfg` to +RR root folder. This file contains reasonable defaults. They could be changed in this file or modified Render apps in `rrControl`. + + +## Debugging + +Current implementation uses dynamically build '.xml' file which is stored in temporary folder accessible by RR. It might make sense to +use this Openpype built file and try to run it via `*__rrServerConsole` executable from command line in case of unforeseeable issues. + +## Known issues + +Currently environment values set in Openpype are not propagated into render jobs on RR. It is studio responsibility to synchronize environment variables from Openpype with all render nodes for now. diff --git a/website/sidebars.js b/website/sidebars.js index 4874782197..c846b04ca7 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -111,6 +111,7 @@ module.exports = { "module_site_sync", "module_deadline", "module_muster", + "module_royalrender", "module_clockify", "module_slack" ], From ad1763a483531365c0e0f1f25a169867f0082460 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 24 May 2023 21:05:31 +0800 Subject: [PATCH 096/446] delivery action add renamed frame number --- openpype/pipeline/delivery.py | 15 ++++++++++++--- openpype/plugins/load/delivery.py | 8 ++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 500f54040a..39d49070df 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -176,7 +176,8 @@ def deliver_sequence( anatomy_data, format_dict, report_items, - log + log, + frame_offset=0 ): """ For Pype2(mainly - works in 3 too) where representation might not contain files. @@ -298,10 +299,18 @@ def deliver_sequence( src = os.path.normpath( os.path.join(dir_path, src_file_name) ) + dsp_index = int(index) + frame_offset + if dsp_index < 0: + msg = "Frame has a smaller number than Frame Offset" + report_items[msg].append(src_file_name) + log.warning("{} <{}>".format(msg, context)) + return report_items, 0 - dst_padding = dst_collection.format("{padding}") % index + dst_padding = dst_collection.format("{padding}") % dsp_index dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) - log.debug("Copying single: {} -> {}".format(src, dst)) + dst = os.path.normpath( + os.path.join(delivery_folder, dst) + ) _copy_file(src, dst) uploaded += 1 diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index d1d5659118..dcb9f094fc 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -88,6 +88,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + frame_offset_value = QtWidgets.QSpinBox() + frame_offset_value.setMinimum(-99) + root_line_edit = QtWidgets.QLineEdit() repre_checkboxes_layout = QtWidgets.QFormLayout() @@ -111,6 +114,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): input_layout.addRow("Selected representations", selected_label) input_layout.addRow("Delivery template", dropdown) input_layout.addRow("Template value", template_label) + input_layout.addRow("Frame Offset", frame_offset_value) input_layout.addRow("Root", root_line_edit) input_layout.addRow("Representations", repre_checkboxes_layout) @@ -138,6 +142,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self.selected_label = selected_label self.template_label = template_label self.dropdown = dropdown + self.frame_offset_value = frame_offset_value self.root_line_edit = root_line_edit self.progress_bar = progress_bar self.text_area = text_area @@ -165,6 +170,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): datetime_data = get_datetime_data() template_name = self.dropdown.currentText() format_dict = get_format_dict(self.anatomy, self.root_line_edit.text()) + frame_offset = self.frame_offset_value.value() for repre in self._representations: if repre["name"] not in selected_repres: continue @@ -217,7 +223,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if not frame: new_report_items, uploaded = deliver_single_file(*args) else: + args.append(frame_offset) new_report_items, uploaded = deliver_sequence(*args) + print(frame_offset) report_items.update(new_report_items) self._update_progress(uploaded) From ca9d3862816826c6d6abc235b31f6f8d76feb265 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 24 May 2023 21:11:19 +0800 Subject: [PATCH 097/446] remove print function --- openpype/plugins/load/delivery.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index dcb9f094fc..54315ce4f7 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -225,7 +225,6 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): else: args.append(frame_offset) new_report_items, uploaded = deliver_sequence(*args) - print(frame_offset) report_items.update(new_report_items) self._update_progress(uploaded) From d6b22badd29c5b15ff3ba2e022f941aacd8ef930 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 2 Jun 2023 15:15:32 +0800 Subject: [PATCH 098/446] add renumber frame button --- openpype/pipeline/delivery.py | 40 +++++++++++++++++++++---------- openpype/plugins/load/delivery.py | 9 ++++++- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 39d49070df..606c926a8d 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -177,6 +177,7 @@ def deliver_sequence( format_dict, report_items, log, + renumber_frame=False, frame_offset=0 ): """ For Pype2(mainly - works in 3 too) where representation might not @@ -299,19 +300,34 @@ def deliver_sequence( src = os.path.normpath( os.path.join(dir_path, src_file_name) ) - dsp_index = int(index) + frame_offset - if dsp_index < 0: - msg = "Frame has a smaller number than Frame Offset" - report_items[msg].append(src_file_name) - log.warning("{} <{}>".format(msg, context)) - return report_items, 0 - dst_padding = dst_collection.format("{padding}") % dsp_index - dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) - dst = os.path.normpath( - os.path.join(delivery_folder, dst) - ) - _copy_file(src, dst) + if renumber_frame: + first_index = src_collection.indexes[0] + dsp_index = (int(index) - first_index) + 1 + dst_padding = dst_collection.format("{padding}") % dsp_index + dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) + dst = os.path.normpath( + os.path.join(delivery_folder, dst) + ) + + _copy_file(src, dst) + + else: + dsp_index = int(index) + frame_offset + if dsp_index < 0: + msg = "Frame has a smaller number than Frame Offset" + report_items[msg].append(src_file_name) + log.warning("{} <{}>".format(msg, context)) + return report_items, 0 + + dst_padding = dst_collection.format("{padding}") % dsp_index + dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) + dst = os.path.normpath( + os.path.join(delivery_folder, dst) + ) + + _copy_file(src, dst) + uploaded += 1 return report_items, uploaded diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 54315ce4f7..68cfb23681 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -1,4 +1,5 @@ import copy +import math from collections import defaultdict from qtpy import QtWidgets, QtCore, QtGui @@ -88,8 +89,10 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + renumber_frame = QtWidgets.QCheckBox() + frame_offset_value = QtWidgets.QSpinBox() - frame_offset_value.setMinimum(-99) + frame_offset_value.setMinimum(-(1<<32)//2) root_line_edit = QtWidgets.QLineEdit() @@ -115,6 +118,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): input_layout.addRow("Delivery template", dropdown) input_layout.addRow("Template value", template_label) input_layout.addRow("Frame Offset", frame_offset_value) + input_layout.addRow("Renumber Frame", renumber_frame) input_layout.addRow("Root", root_line_edit) input_layout.addRow("Representations", repre_checkboxes_layout) @@ -143,6 +147,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self.template_label = template_label self.dropdown = dropdown self.frame_offset_value = frame_offset_value + self.renumber_frame = renumber_frame self.root_line_edit = root_line_edit self.progress_bar = progress_bar self.text_area = text_area @@ -170,6 +175,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): datetime_data = get_datetime_data() template_name = self.dropdown.currentText() format_dict = get_format_dict(self.anatomy, self.root_line_edit.text()) + renumber_frame = self.renumber_frame.isChecked() frame_offset = self.frame_offset_value.value() for repre in self._representations: if repre["name"] not in selected_repres: @@ -223,6 +229,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if not frame: new_report_items, uploaded = deliver_single_file(*args) else: + args.append(renumber_frame) args.append(frame_offset) new_report_items, uploaded = deliver_sequence(*args) report_items.update(new_report_items) From 199be9fb72619453459bfa1420ad70ceed40bfa1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 2 Jun 2023 15:17:06 +0800 Subject: [PATCH 099/446] hound fix --- openpype/plugins/load/delivery.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 68cfb23681..f8acc85cf9 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -1,5 +1,4 @@ import copy -import math from collections import defaultdict from qtpy import QtWidgets, QtCore, QtGui @@ -92,7 +91,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): renumber_frame = QtWidgets.QCheckBox() frame_offset_value = QtWidgets.QSpinBox() - frame_offset_value.setMinimum(-(1<<32)//2) + frame_offset_value.setMinimum(-(1 << 32) // 2) root_line_edit = QtWidgets.QLineEdit() From d7fd9c8e18b7f47fa43c559038d08d71e54ab87c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 5 Jun 2023 12:36:18 +0200 Subject: [PATCH 100/446] :recycle: non-optional access to rep data --- openpype/pipeline/farm/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/tools.py b/openpype/pipeline/farm/tools.py index 6f9e0ac393..f3acac7a32 100644 --- a/openpype/pipeline/farm/tools.py +++ b/openpype/pipeline/farm/tools.py @@ -47,7 +47,7 @@ def from_published_scene(instance, replace_in_path=True): # determine published path from Anatomy. template_data = workfile_instance.data.get("anatomyData") - rep = workfile_instance.data.get("representations")[0] + rep = workfile_instance.data["representations"][0] template_data["representation"] = rep.get("name") template_data["ext"] = rep.get("ext") template_data["comment"] = None From 89070dc8e34a7351158253161fb66f47fc302c36 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 6 Jun 2023 18:11:55 +0800 Subject: [PATCH 101/446] renumber first frame option --- openpype/pipeline/delivery.py | 23 +++++++++-------------- openpype/plugins/load/delivery.py | 17 ++++++++++------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 606c926a8d..eb28db26b9 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -178,7 +178,7 @@ def deliver_sequence( report_items, log, renumber_frame=False, - frame_offset=0 + re_frame_value=0 ): """ For Pype2(mainly - works in 3 too) where representation might not contain files. @@ -302,20 +302,9 @@ def deliver_sequence( ) if renumber_frame: - first_index = src_collection.indexes[0] - dsp_index = (int(index) - first_index) + 1 - dst_padding = dst_collection.format("{padding}") % dsp_index - dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) - dst = os.path.normpath( - os.path.join(delivery_folder, dst) - ) - - _copy_file(src, dst) - - else: - dsp_index = int(index) + frame_offset + dsp_index = (re_frame_value - int(index)) + 1 if dsp_index < 0: - msg = "Frame has a smaller number than Frame Offset" + msg = "Renumber frame has a smaller number than original frame" report_items[msg].append(src_file_name) log.warning("{} <{}>".format(msg, context)) return report_items, 0 @@ -328,6 +317,12 @@ def deliver_sequence( _copy_file(src, dst) + else: + dst_padding = dst_collection.format("{padding}") % index + dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) + log.debug("Copying single: {} -> {}".format(src, dst)) + _copy_file(src, dst) + uploaded += 1 return report_items, uploaded diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index f8acc85cf9..826b4ad95c 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -90,8 +90,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): renumber_frame = QtWidgets.QCheckBox() - frame_offset_value = QtWidgets.QSpinBox() - frame_offset_value.setMinimum(-(1 << 32) // 2) + renumber_frame_value = QtWidgets.QSpinBox() + renumber_frame_value.setMinimum(-(1 << 32) // 2) root_line_edit = QtWidgets.QLineEdit() @@ -116,8 +116,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): input_layout.addRow("Selected representations", selected_label) input_layout.addRow("Delivery template", dropdown) input_layout.addRow("Template value", template_label) - input_layout.addRow("Frame Offset", frame_offset_value) input_layout.addRow("Renumber Frame", renumber_frame) + input_layout.addRow("Renumber start frame", renumber_frame_value) input_layout.addRow("Root", root_line_edit) input_layout.addRow("Representations", repre_checkboxes_layout) @@ -145,7 +145,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self.selected_label = selected_label self.template_label = template_label self.dropdown = dropdown - self.frame_offset_value = frame_offset_value + self.renumber_frame_value = renumber_frame_value self.renumber_frame = renumber_frame self.root_line_edit = root_line_edit self.progress_bar = progress_bar @@ -175,7 +175,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): template_name = self.dropdown.currentText() format_dict = get_format_dict(self.anatomy, self.root_line_edit.text()) renumber_frame = self.renumber_frame.isChecked() - frame_offset = self.frame_offset_value.value() + frame_offset = self.renumber_frame_value.value() for repre in self._representations: if repre["name"] not in selected_repres: continue @@ -228,8 +228,11 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if not frame: new_report_items, uploaded = deliver_single_file(*args) else: - args.append(renumber_frame) - args.append(frame_offset) + optional_args = [ + renumber_frame, + frame_offset + ] + args.extend(optional_args) new_report_items, uploaded = deliver_sequence(*args) report_items.update(new_report_items) self._update_progress(uploaded) From 6926ebd546ca396c5591f27a85c61096979688a8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 6 Jun 2023 18:17:41 +0800 Subject: [PATCH 102/446] add msg --- openpype/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index eb28db26b9..383959c3f8 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -304,7 +304,7 @@ def deliver_sequence( if renumber_frame: dsp_index = (re_frame_value - int(index)) + 1 if dsp_index < 0: - msg = "Renumber frame has a smaller number than original frame" + msg = "Renumber frame has a smaller number than original frame" #noqa report_items[msg].append(src_file_name) log.warning("{} <{}>".format(msg, context)) return report_items, 0 From 2994018817d447943a1c941b096b2953158c31d8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 6 Jun 2023 18:19:14 +0800 Subject: [PATCH 103/446] hound fix --- openpype/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 383959c3f8..0df03bf8f8 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -304,7 +304,7 @@ def deliver_sequence( if renumber_frame: dsp_index = (re_frame_value - int(index)) + 1 if dsp_index < 0: - msg = "Renumber frame has a smaller number than original frame" #noqa + msg = "Renumber frame has a smaller number than original frame" # noqa report_items[msg].append(src_file_name) log.warning("{} <{}>".format(msg, context)) return report_items, 0 From 705368897936811a33f18d92b33939028cf0f85b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Jun 2023 16:20:30 +0200 Subject: [PATCH 104/446] :bug: fix rendering from published file in Deadline --- openpype/modules/deadline/abstract_submit_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index c2cc518e40..525e112b71 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -430,7 +430,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): file_path = None if self.use_published: if not self.import_reference: - file_path = get_published_workfile_instance(instance) + file_path = self.from_published_scene(context) else: self.log.info("use the scene with imported reference for rendering") # noqa file_path = context.data["currentFile"] From 2851b3230ec4ae332a0106913491fcbadfd6a179 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Jun 2023 17:29:23 +0200 Subject: [PATCH 105/446] :bug: fix collections files must be lists, fixed camera handling of single frame renders --- openpype/pipeline/farm/pyblish_functions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 8be0887b0d..0ace02edb9 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -550,14 +550,19 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, if len(cols) != 1: raise ValueError("Only one image sequence type is expected.") # noqa: E501 ext = cols[0].tail.lstrip(".") - col = cols[0].head + col = list(cols[0]) # create subset name `familyTaskSubset_AOV` group_name = 'render{}{}{}{}'.format( task[0].upper(), task[1:], subset[0].upper(), subset[1:]) - cam = [c for c in cameras if c in col] + # if there are multiple cameras, we need to add camera name + if isinstance(col, (list, tuple)): + cam = [c for c in cameras if c in col[0]] + else: + # in case of single frame + cam = [c for c in cameras if c in col] if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) From 8030ee9c71fcbad57802aff9f8296569e8c00d4d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 8 Jun 2023 19:05:21 +0800 Subject: [PATCH 106/446] update indexes for dst_collections --- openpype/pipeline/delivery.py | 56 +++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 0df03bf8f8..3b030855b1 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -282,10 +282,26 @@ def deliver_sequence( delivery_folder = os.path.dirname(delivery_path) dst_head, dst_tail = delivery_path.split(frame_indicator) dst_padding = src_collection.padding + dsp_indexes = set() + if renumber_frame: + for idx in src_collection.indexes: + dsp_list = [] + dsp_idx = (re_frame_value - int(idx)) + 1 + if dsp_idx < 0: + msg = "Renumber frame has a smaller number than original frame" # noqa + report_items[msg].append(dsp_idx) + log.warning("{} <{}>".format(msg, context)) + return report_items, 0 + dsp_list.append(dsp_idx) + dsp_indexes.add(dsp_list) + else: + dsp_indexes = set([idx for idx in src_collection.indexes]) + dst_collection = clique.Collection( head=dst_head, tail=dst_tail, - padding=dst_padding + padding=dst_padding, + indexes=dsp_indexes ) if not os.path.exists(delivery_folder): @@ -294,32 +310,34 @@ def deliver_sequence( src_head = src_collection.head src_tail = src_collection.tail uploaded = 0 - for index in src_collection.indexes: - src_padding = src_collection.format("{padding}") % index - src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) - src = os.path.normpath( - os.path.join(dir_path, src_file_name) - ) - - if renumber_frame: - dsp_index = (re_frame_value - int(index)) + 1 - if dsp_index < 0: - msg = "Renumber frame has a smaller number than original frame" # noqa - report_items[msg].append(src_file_name) - log.warning("{} <{}>".format(msg, context)) - return report_items, 0 - - dst_padding = dst_collection.format("{padding}") % dsp_index + if renumber_frame: + for (src_idx, dst_idx) in zip( + src_collection.indexes, dst_collection.indexes): + src_padding = src_collection.format("{padding}") % src_idx + src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) + src = os.path.normpath( + os.path.join(dir_path, src_file_name) + ) + dst_padding = dst_collection.format("{padding}") % dst_idx dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) dst = os.path.normpath( os.path.join(delivery_folder, dst) ) - + log.debug("Copying single: {} -> {}".format(src, dst)) _copy_file(src, dst) + else: + for index in src_collection.indexes: + src_padding = src_collection.format("{padding}") % index + src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) + src = os.path.normpath( + os.path.join(dir_path, src_file_name) + ) - else: dst_padding = dst_collection.format("{padding}") % index dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) + dst = os.path.normpath( + os.path.join(delivery_folder, dst) + ) log.debug("Copying single: {} -> {}".format(src, dst)) _copy_file(src, dst) From 95a4a60fd2632496f501b8971ca24743fdc35afe Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 8 Jun 2023 19:07:37 +0800 Subject: [PATCH 107/446] hound fix --- openpype/pipeline/delivery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 3b030855b1..74fe205aa3 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -311,8 +311,8 @@ def deliver_sequence( src_tail = src_collection.tail uploaded = 0 if renumber_frame: - for (src_idx, dst_idx) in zip( - src_collection.indexes, dst_collection.indexes): + for (src_idx, dst_idx) in zip(src_collection.indexes, + dst_collection.indexes): src_padding = src_collection.format("{padding}") % src_idx src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) src = os.path.normpath( From 01b5e6b9500a8b4460429427ee06f6021208be97 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 9 Jun 2023 17:30:35 +0200 Subject: [PATCH 108/446] OP-6037 - extracted generic logic from working Nuke implementation Will be used as a base for Maya impl too. --- openpype/modules/royalrender/lib.py | 301 ++++++++++++++++ .../publish/create_nuke_royalrender_job.py | 320 ++---------------- 2 files changed, 326 insertions(+), 295 deletions(-) create mode 100644 openpype/modules/royalrender/lib.py diff --git a/openpype/modules/royalrender/lib.py b/openpype/modules/royalrender/lib.py new file mode 100644 index 0000000000..ce78b1a738 --- /dev/null +++ b/openpype/modules/royalrender/lib.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- +"""Submitting render job to RoyalRender.""" +import os +import re +import platform +from datetime import datetime + +import pyblish.api +from openpype.tests.lib import is_in_tests +from openpype.pipeline.publish.lib import get_published_workfile_instance +from openpype.pipeline.publish import KnownPublishError +from openpype.modules.royalrender.api import Api as rrApi +from openpype.modules.royalrender.rr_job import ( + RRJob, CustomAttribute, get_rr_platform) +from openpype.lib import ( + is_running_from_build, + BoolDef, + NumberDef, +) +from openpype.pipeline import OpenPypePyblishPluginMixin + + +class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): + """Creates separate rendering job for Royal Render""" + label = "Create Nuke Render job in RR" + order = pyblish.api.IntegratorOrder + 0.1 + hosts = ["nuke"] + families = ["render", "prerender"] + targets = ["local"] + optional = True + + priority = 50 + chunk_size = 1 + concurrent_tasks = 1 + use_gpu = True + use_published = True + + @classmethod + def get_attribute_defs(cls): + return [ + NumberDef( + "priority", + label="Priority", + default=cls.priority, + decimals=0 + ), + NumberDef( + "chunk", + label="Frames Per Task", + default=cls.chunk_size, + decimals=0, + minimum=1, + maximum=1000 + ), + NumberDef( + "concurrency", + label="Concurrency", + default=cls.concurrent_tasks, + decimals=0, + minimum=1, + maximum=10 + ), + BoolDef( + "use_gpu", + default=cls.use_gpu, + label="Use GPU" + ), + BoolDef( + "suspend_publish", + default=False, + label="Suspend publish" + ), + BoolDef( + "use_published", + default=cls.use_published, + label="Use published workfile" + ) + ] + + def __init__(self, *args, **kwargs): + self._instance = None + self._rr_root = None + self.scene_path = None + self.job = None + self.submission_parameters = None + self.rr_api = None + + def process(self, instance): + if not instance.data.get("farm"): + self.log.info("Skipping local instance.") + return + + instance.data["attributeValues"] = self.get_attr_values_from_data( + instance.data) + + # add suspend_publish attributeValue to instance data + instance.data["suspend_publish"] = instance.data["attributeValues"][ + "suspend_publish"] + + context = instance.context + self._instance = instance + + self._rr_root = self._resolve_rr_path(context, instance.data.get( + "rrPathName")) # noqa + self.log.debug(self._rr_root) + if not self._rr_root: + raise KnownPublishError( + ("Missing RoyalRender root. " + "You need to configure RoyalRender module.")) + + self.rr_api = rrApi(self._rr_root) + + self.scene_path = context.data["currentFile"] + if self.use_published: + file_path = get_published_workfile_instance(context) + + # fallback if nothing was set + if not file_path: + self.log.warning("Falling back to workfile") + file_path = context.data["currentFile"] + + self.scene_path = file_path + self.log.info( + "Using published scene for render {}".format(self.scene_path) + ) + + if not self._instance.data.get("expectedFiles"): + self._instance.data["expectedFiles"] = [] + + if not self._instance.data.get("rrJobs"): + self._instance.data["rrJobs"] = [] + + self._instance.data["outputDir"] = os.path.dirname( + self._instance.data["path"]).replace("\\", "/") + + def get_job(self, instance, script_path, render_path, node_name): + """Get RR job based on current instance. + + Args: + script_path (str): Path to Nuke script. + render_path (str): Output path. + node_name (str): Name of the render node. + + Returns: + RRJob: RoyalRender Job instance. + + """ + start_frame = int(instance.data["frameStartHandle"]) + end_frame = int(instance.data["frameEndHandle"]) + + batch_name = os.path.basename(script_path) + jobname = "%s - %s" % (batch_name, self._instance.name) + if is_in_tests(): + batch_name += datetime.now().strftime("%d%m%Y%H%M%S") + + render_dir = os.path.normpath(os.path.dirname(render_path)) + output_filename_0 = self.preview_fname(render_path) + file_name, file_ext = os.path.splitext( + os.path.basename(output_filename_0)) + + custom_attributes = [] + if is_running_from_build(): + custom_attributes = [ + CustomAttribute( + name="OpenPypeVersion", + value=os.environ.get("OPENPYPE_VERSION")) + ] + + # this will append expected files to instance as needed. + expected_files = self.expected_files( + instance, render_path, start_frame, end_frame) + instance.data["expectedFiles"].extend(expected_files) + + job = RRJob( + Software="", + Renderer="", + SeqStart=int(start_frame), + SeqEnd=int(end_frame), + SeqStep=int(instance.data.get("byFrameStep", 1)), + SeqFileOffset=0, + Version=0, + SceneName=script_path, + IsActive=True, + ImageDir=render_dir.replace("\\", "/"), + ImageFilename=file_name, + ImageExtension=file_ext, + ImagePreNumberLetter="", + ImageSingleOutputFile=False, + SceneOS=get_rr_platform(), + Layer=node_name, + SceneDatabaseDir=script_path, + CustomSHotName=jobname, + CompanyProjectName=instance.context.data["projectName"], + ImageWidth=instance.data["resolutionWidth"], + ImageHeight=instance.data["resolutionHeight"], + CustomAttributes=custom_attributes + ) + + return job + + def update_job_with_host_specific(self, instance, job): + """Host specific mapping for RRJob""" + raise NotImplementedError + + @staticmethod + def _resolve_rr_path(context, rr_path_name): + # type: (pyblish.api.Context, str) -> str + rr_settings = ( + context.data + ["system_settings"] + ["modules"] + ["royalrender"] + ) + try: + default_servers = rr_settings["rr_paths"] + project_servers = ( + context.data + ["project_settings"] + ["royalrender"] + ["rr_paths"] + ) + rr_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + + except (AttributeError, KeyError): + # Handle situation were we had only one url for royal render. + return context.data["defaultRRPath"][platform.system().lower()] + + return rr_servers[rr_path_name][platform.system().lower()] + + def expected_files(self, instance, path, start_frame, end_frame): + """Get expected files. + + This function generate expected files from provided + path and start/end frames. + + It was taken from Deadline module, but this should be + probably handled better in collector to support more + flexible scenarios. + + Args: + instance (Instance) + path (str): Output path. + start_frame (int): Start frame. + end_frame (int): End frame. + + Returns: + list: List of expected files. + + """ + if instance.data.get("expectedFiles"): + return instance.data["expectedFiles"] + + dir_name = os.path.dirname(path) + file = os.path.basename(path) + + expected_files = [] + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + expected_files.append(path) + return expected_files + + if self._instance.data.get("slate"): + start_frame -= 1 + + expected_files.extend( + os.path.join(dir_name, (file % i)).replace("\\", "/") + for i in range(start_frame, (end_frame + 1)) + ) + return expected_files + + def preview_fname(self, path): + """Return output file path with #### for padding. + + RR requires the path to be formatted with # in place of numbers. + For example `/path/to/render.####.png` + + Args: + path (str): path to rendered images + + Returns: + str + + """ + self.log.debug("_ path: `{}`".format(path)) + if "%" in path: + search_results = re.search(r"(%0)(\d)(d.)", path).groups() + self.log.debug("_ search_results: `{}`".format(search_results)) + return int(search_results[1]) + if "#" in path: + self.log.debug("_ path: `{}`".format(path)) + return path diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py index a90c4c4f83..62636a6744 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py @@ -1,137 +1,18 @@ # -*- coding: utf-8 -*- """Submitting render job to RoyalRender.""" -import os import re -import platform -from datetime import datetime -import pyblish.api -from openpype.tests.lib import is_in_tests -from openpype.pipeline.publish.lib import get_published_workfile_instance -from openpype.pipeline.publish import KnownPublishError -from openpype.modules.royalrender.api import Api as rrApi -from openpype.modules.royalrender.rr_job import ( - RRJob, CustomAttribute, get_rr_platform) -from openpype.lib import ( - is_running_from_build, - BoolDef, - NumberDef, -) -from openpype.pipeline import OpenPypePyblishPluginMixin +from openpype.modules.royalrender import lib -class CreateNukeRoyalRenderJob(pyblish.api.InstancePlugin, - OpenPypePyblishPluginMixin): +class CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob): """Creates separate rendering job for Royal Render""" label = "Create Nuke Render job in RR" - order = pyblish.api.IntegratorOrder + 0.1 hosts = ["nuke"] families = ["render", "prerender"] - targets = ["local"] - optional = True - - priority = 50 - chunk_size = 1 - concurrent_tasks = 1 - use_gpu = True - use_published = True - - @classmethod - def get_attribute_defs(cls): - return [ - NumberDef( - "priority", - label="Priority", - default=cls.priority, - decimals=0 - ), - NumberDef( - "chunk", - label="Frames Per Task", - default=cls.chunk_size, - decimals=0, - minimum=1, - maximum=1000 - ), - NumberDef( - "concurrency", - label="Concurrency", - default=cls.concurrent_tasks, - decimals=0, - minimum=1, - maximum=10 - ), - BoolDef( - "use_gpu", - default=cls.use_gpu, - label="Use GPU" - ), - BoolDef( - "suspend_publish", - default=False, - label="Suspend publish" - ), - BoolDef( - "use_published", - default=cls.use_published, - label="Use published workfile" - ) - ] - - def __init__(self, *args, **kwargs): - self._instance = None - self._rr_root = None - self.scene_path = None - self.job = None - self.submission_parameters = None - self.rr_api = None def process(self, instance): - if not instance.data.get("farm"): - self.log.info("Skipping local instance.") - return - - instance.data["attributeValues"] = self.get_attr_values_from_data( - instance.data) - - # add suspend_publish attributeValue to instance data - instance.data["suspend_publish"] = instance.data["attributeValues"][ - "suspend_publish"] - - context = instance.context - self._instance = instance - - self._rr_root = self._resolve_rr_path(context, instance.data.get( - "rrPathName")) # noqa - self.log.debug(self._rr_root) - if not self._rr_root: - raise KnownPublishError( - ("Missing RoyalRender root. " - "You need to configure RoyalRender module.")) - - self.rr_api = rrApi(self._rr_root) - - self.scene_path = context.data["currentFile"] - if self.use_published: - file_path = get_published_workfile_instance(context) - - # fallback if nothing was set - if not file_path: - self.log.warning("Falling back to workfile") - file_path = context.data["currentFile"] - - self.scene_path = file_path - self.log.info( - "Using published scene for render {}".format(self.scene_path) - ) - - if not self._instance.data.get("expectedFiles"): - self._instance.data["expectedFiles"] = [] - - if not self._instance.data.get("rrJobs"): - self._instance.data["rrJobs"] = [] - - self._instance.data["rrJobs"].extend(self.create_jobs()) + super(CreateNukeRoyalRenderJob, self).process(instance) # redefinition of families if "render" in self._instance.data["family"]: @@ -141,26 +22,37 @@ class CreateNukeRoyalRenderJob(pyblish.api.InstancePlugin, self._instance.data["family"] = "write" self._instance.data["families"].insert(0, "prerender") - self._instance.data["outputDir"] = os.path.dirname( - self._instance.data["path"]).replace("\\", "/") + jobs = self.create_jobs(self._instance) + for job in jobs: + job = self.update_job_with_host_specific(instance, job) - def create_jobs(self): - submit_frame_start = int(self._instance.data["frameStartHandle"]) - submit_frame_end = int(self._instance.data["frameEndHandle"]) + instance.data["rrJobs"] += jobs + def update_job_with_host_specific(self, instance, job): + nuke_version = re.search( + r"\d+\.\d+", self._instance.context.data.get("hostVersion")) + + job.Software = "Nuke" + job.Version = nuke_version.group() + + return job + + def create_jobs(self, instance): + """Nuke creates multiple RR jobs - for baking etc.""" # get output path - render_path = self._instance.data['path'] + render_path = instance.data['path'] + self.log.info("render::{}".format(render_path)) + self.log.info("expected::{}".format(instance.data.get("expectedFiles"))) script_path = self.scene_path node = self._instance.data["transientData"]["node"] # main job jobs = [ self.get_job( + instance, script_path, render_path, - node.name(), - submit_frame_start, - submit_frame_end, + node.name() ) ] @@ -170,172 +62,10 @@ class CreateNukeRoyalRenderJob(pyblish.api.InstancePlugin, exe_node_name = baking_script["bakeWriteNodeName"] jobs.append(self.get_job( + instance, script_path, render_path, - exe_node_name, - submit_frame_start, - submit_frame_end + exe_node_name )) return jobs - - def get_job(self, script_path, render_path, - node_name, start_frame, end_frame): - """Get RR job based on current instance. - - Args: - script_path (str): Path to Nuke script. - render_path (str): Output path. - node_name (str): Name of the render node. - start_frame (int): Start frame. - end_frame (int): End frame. - - Returns: - RRJob: RoyalRender Job instance. - - """ - render_dir = os.path.normpath(os.path.dirname(render_path)) - batch_name = os.path.basename(script_path) - jobname = "%s - %s" % (batch_name, self._instance.name) - if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - - output_filename_0 = self.preview_fname(render_path) - file_name, file_ext = os.path.splitext( - os.path.basename(output_filename_0)) - - custom_attributes = [] - if is_running_from_build(): - custom_attributes = [ - CustomAttribute( - name="OpenPypeVersion", - value=os.environ.get("OPENPYPE_VERSION")) - ] - - nuke_version = re.search( - r"\d+\.\d+", self._instance.context.data.get("hostVersion")) - - # this will append expected files to instance as needed. - expected_files = self.expected_files( - render_path, start_frame, end_frame) - self._instance.data["expectedFiles"].extend(expected_files) - - job = RRJob( - Software="Nuke", - Renderer="", - SeqStart=int(start_frame), - SeqEnd=int(end_frame), - SeqStep=int(self._instance.data.get("byFrameStep", 1)), - SeqFileOffset=0, - Version=nuke_version.group(), - SceneName=script_path, - IsActive=True, - ImageDir=render_dir.replace("\\", "/"), - ImageFilename=file_name, - ImageExtension=file_ext, - ImagePreNumberLetter="", - ImageSingleOutputFile=False, - SceneOS=get_rr_platform(), - Layer=node_name, - SceneDatabaseDir=script_path, - CustomSHotName=jobname, - CompanyProjectName=self._instance.context.data["projectName"], - ImageWidth=self._instance.data["resolutionWidth"], - ImageHeight=self._instance.data["resolutionHeight"], - CustomAttributes=custom_attributes - ) - - return job - - @staticmethod - def _resolve_rr_path(context, rr_path_name): - # type: (pyblish.api.Context, str) -> str - rr_settings = ( - context.data - ["system_settings"] - ["modules"] - ["royalrender"] - ) - try: - default_servers = rr_settings["rr_paths"] - project_servers = ( - context.data - ["project_settings"] - ["royalrender"] - ["rr_paths"] - ) - rr_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers - } - - except (AttributeError, KeyError): - # Handle situation were we had only one url for royal render. - return context.data["defaultRRPath"][platform.system().lower()] - - return rr_servers[rr_path_name][platform.system().lower()] - - def expected_files(self, path, start_frame, end_frame): - """Get expected files. - - This function generate expected files from provided - path and start/end frames. - - It was taken from Deadline module, but this should be - probably handled better in collector to support more - flexible scenarios. - - Args: - path (str): Output path. - start_frame (int): Start frame. - end_frame (int): End frame. - - Returns: - list: List of expected files. - - """ - dir_name = os.path.dirname(path) - file = os.path.basename(path) - - expected_files = [] - - if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] - - if "%" not in file: - expected_files.append(path) - return expected_files - - if self._instance.data.get("slate"): - start_frame -= 1 - - expected_files.extend( - os.path.join(dir_name, (file % i)).replace("\\", "/") - for i in range(start_frame, (end_frame + 1)) - ) - return expected_files - - def preview_fname(self, path): - """Return output file path with #### for padding. - - Deadline requires the path to be formatted with # in place of numbers. - For example `/path/to/render.####.png` - - Args: - path (str): path to rendered images - - Returns: - str - - """ - self.log.debug("_ path: `{}`".format(path)) - if "%" in path: - search_results = re.search(r"(%0)(\d)(d.)", path).groups() - self.log.debug("_ search_results: `{}`".format(search_results)) - return int(search_results[1]) - if "#" in path: - self.log.debug("_ path: `{}`".format(path)) - return path From 644b734585cadecb32f953b4efa3c4859150bebb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 9 Jun 2023 17:32:11 +0200 Subject: [PATCH 109/446] OP-6037 - first iteration of Maya to RR Uses generic logic, probably needs fine tuning. --- .../publish/create_maya_royalrender_job.py | 160 +++--------------- 1 file changed, 23 insertions(+), 137 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 1bb2785dd6..5ad3c90bdc 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -1,156 +1,42 @@ # -*- coding: utf-8 -*- """Submitting render job to RoyalRender.""" +# -*- coding: utf-8 -*- +"""Submitting render job to RoyalRender.""" import os -import sys -import platform -from maya.OpenMaya import MGlobal # noqa -import pyblish.api -from openpype.lib import is_running_from_build -from openpype.pipeline.publish.lib import get_published_workfile_instance -from openpype.pipeline.publish import KnownPublishError -from openpype.modules.royalrender.api import Api as rrApi -from openpype.modules.royalrender.rr_job import RRJob, CustomAttribute +from maya.OpenMaya import MGlobal + +from openpype.modules.royalrender import lib from openpype.pipeline.farm.tools import iter_expected_files -class CreateMayaRoyalRenderJob(pyblish.api.InstancePlugin): +class CreateMayaRoyalRenderJob(lib.BaseCreateRoyalRenderJob): label = "Create Maya Render job in RR" - order = pyblish.api.IntegratorOrder + 0.1 families = ["renderlayer"] - targets = ["local"] - use_published = True - def __init__(self, *args, **kwargs): - self._instance = None - self._rrRoot = None - self.scene_path = None - self.job = None - self.submission_parameters = None - self.rr_api = None - - def get_job(self): - """Prepare job payload. - - Returns: - RRJob: RoyalRender job payload. - """ - def get_rr_platform(): - if sys.platform.lower() in ["win32", "win64"]: - return "windows" - elif sys.platform.lower() == "darwin": - return "mac" - else: - return "linux" - - expected_files = self._instance.data["expectedFiles"] - first_file = next(iter_expected_files(expected_files)) - output_dir = os.path.dirname(first_file) - self._instance.data["outputDir"] = output_dir - workspace = self._instance.context.data["workspaceDir"] - default_render_file = ( - self._instance.context.data - ['project_settings'] - ['maya'] - ['RenderSettings'] - ['default_render_image_folder'] - ) - # file_name = os.path.basename(self.scene_path) - dir_name = os.path.join(workspace, default_render_file) - layer = self._instance.data["setMembers"] # type: str - layer_name = layer.removeprefix("rs_") - - custom_attributes = [] - if is_running_from_build(): - custom_attributes = [ - CustomAttribute( - name="OpenPypeVersion", - value=os.environ.get("OPENPYPE_VERSION")) - ] - - job = RRJob( - Software="Maya", - Renderer=self._instance.data["renderer"], - SeqStart=int(self._instance.data["frameStartHandle"]), - SeqEnd=int(self._instance.data["frameEndHandle"]), - SeqStep=int(self._instance.data["byFrameStep"]), - SeqFileOffset=0, - Version="{0:.2f}".format(MGlobal.apiVersion() / 10000), - SceneName=self.scene_path, - IsActive=True, - ImageDir=dir_name, - ImageFilename="{}.".format(layer_name), - ImageExtension=os.path.splitext(first_file)[1], - ImagePreNumberLetter=".", - ImageSingleOutputFile=False, - SceneOS=get_rr_platform(), - Camera=self._instance.data["cameras"][0], - Layer=layer_name, - SceneDatabaseDir=workspace, - CustomSHotName=self._instance.context.data["asset"], - CompanyProjectName=self._instance.context.data["projectName"], - ImageWidth=self._instance.data["resolutionWidth"], - ImageHeight=self._instance.data["resolutionHeight"], - CustomAttributes=custom_attributes - ) + def update_job_with_host_specific(self, instance, job): + job.Software = "Maya" + job.Version = "{0:.2f}".format(MGlobal.apiVersion() / 10000) + job.Camera = instance.data["cameras"][0], + workspace = instance.context.data["workspaceDir"] + job.SceneDatabaseDir = workspace return job def process(self, instance): """Plugin entry point.""" - self._instance = instance - context = instance.context + super(CreateMayaRoyalRenderJob, self).process(instance) - self._rr_root = self._resolve_rr_path(context, instance.data.get("rrPathName")) # noqa - self.log.debug(self._rr_root) - if not self._rr_root: - raise KnownPublishError( - ("Missing RoyalRender root. " - "You need to configure RoyalRender module.")) + expected_files = self._instance.data["expectedFiles"] + first_file_path = next(iter_expected_files(expected_files)) + output_dir = os.path.dirname(first_file_path) + self._instance.data["outputDir"] = output_dir - self.rr_api = rrApi(self._rr_root) + layer = self._instance.data["setMembers"] # type: str + layer_name = layer.removeprefix("rs_") - file_path = None - if self.use_published: - file_path = get_published_workfile_instance(context) + job = self.get_job(instance, self.scene_path, first_file_path, + layer_name) + job = self.update_job_with_host_specific(instance, job) - # fallback if nothing was set - if not file_path: - self.log.warning("Falling back to workfile") - file_path = context.data["currentFile"] - - self.scene_path = file_path - - if not self._instance.data.get("rrJobs"): - self._instance.data["rrJobs"] = [] - - self._instance.data["rrJobs"] += self.get_job() - - @staticmethod - def _resolve_rr_path(context, rr_path_name): - # type: (pyblish.api.Context, str) -> str - rr_settings = ( - context.data - ["system_settings"] - ["modules"] - ["royalrender"] - ) - try: - default_servers = rr_settings["rr_paths"] - project_servers = ( - context.data - ["project_settings"] - ["royalrender"] - ["rr_paths"] - ) - rr_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers - } - - except (AttributeError, KeyError): - # Handle situation were we had only one url for royal render. - return context.data["defaultRRPath"][platform.system().lower()] - - return rr_servers[rr_path_name][platform.system().lower()] + instance.data["rrJobs"] += job From 4576a6fee630d3bc9df494ef08302cb53ad32b85 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 13 Jun 2023 00:38:48 +0800 Subject: [PATCH 110/446] clean up the pipeline script --- openpype/pipeline/delivery.py | 62 +++++++++++++---------------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 74fe205aa3..2edfec19b2 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -177,8 +177,8 @@ def deliver_sequence( format_dict, report_items, log, - renumber_frame=False, - re_frame_value=0 + renumber_frame=True, + re_frame_value=12 ): """ For Pype2(mainly - works in 3 too) where representation might not contain files. @@ -282,26 +282,10 @@ def deliver_sequence( delivery_folder = os.path.dirname(delivery_path) dst_head, dst_tail = delivery_path.split(frame_indicator) dst_padding = src_collection.padding - dsp_indexes = set() - if renumber_frame: - for idx in src_collection.indexes: - dsp_list = [] - dsp_idx = (re_frame_value - int(idx)) + 1 - if dsp_idx < 0: - msg = "Renumber frame has a smaller number than original frame" # noqa - report_items[msg].append(dsp_idx) - log.warning("{} <{}>".format(msg, context)) - return report_items, 0 - dsp_list.append(dsp_idx) - dsp_indexes.add(dsp_list) - else: - dsp_indexes = set([idx for idx in src_collection.indexes]) - dst_collection = clique.Collection( head=dst_head, tail=dst_tail, - padding=dst_padding, - indexes=dsp_indexes + padding=dst_padding ) if not os.path.exists(delivery_folder): @@ -310,34 +294,32 @@ def deliver_sequence( src_head = src_collection.head src_tail = src_collection.tail uploaded = 0 - if renumber_frame: - for (src_idx, dst_idx) in zip(src_collection.indexes, - dst_collection.indexes): - src_padding = src_collection.format("{padding}") % src_idx - src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) - src = os.path.normpath( - os.path.join(dir_path, src_file_name) - ) - dst_padding = dst_collection.format("{padding}") % dst_idx + for index in src_collection.indexes: + src_padding = src_collection.format("{padding}") % index + src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) + src = os.path.normpath( + os.path.join(dir_path, src_file_name) + ) + + if renumber_frame: + dsp_index = (re_frame_value - int(index)) + 1 + if dsp_index < 0: + msg = "Renumber frame has a smaller number than original frame" # noqa + report_items[msg].append(src_file_name) + log.warning("{} <{}>".format(msg, context)) + return report_items, 0 + + dst_padding = dst_collection.format("{padding}") % dsp_index dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) dst = os.path.normpath( os.path.join(delivery_folder, dst) ) - log.debug("Copying single: {} -> {}".format(src, dst)) - _copy_file(src, dst) - else: - for index in src_collection.indexes: - src_padding = src_collection.format("{padding}") % index - src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) - src = os.path.normpath( - os.path.join(dir_path, src_file_name) - ) + _copy_file(src, dst) + + else: dst_padding = dst_collection.format("{padding}") % index dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) - dst = os.path.normpath( - os.path.join(delivery_folder, dst) - ) log.debug("Copying single: {} -> {}".format(src, dst)) _copy_file(src, dst) From cddbfc02f8153b23e530942fab7c48222413cb38 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 13 Jun 2023 06:52:00 +0800 Subject: [PATCH 111/446] set the right default value in deliver args --- openpype/pipeline/delivery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 2edfec19b2..0df03bf8f8 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -177,8 +177,8 @@ def deliver_sequence( format_dict, report_items, log, - renumber_frame=True, - re_frame_value=12 + renumber_frame=False, + re_frame_value=0 ): """ For Pype2(mainly - works in 3 too) where representation might not contain files. From 9cb121601a27727dd73df8b3241bd72c666edda0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 14 Jun 2023 10:13:48 +0200 Subject: [PATCH 112/446] Resolved conflict --- .../plugins/publish/collect_rr_path_from_instance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index 6a3dc276f3..c2ba1e2c19 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -5,9 +5,9 @@ import pyblish.api class CollectRRPathFromInstance(pyblish.api.InstancePlugin): """Collect RR Path from instance.""" - order = pyblish.api.CollectorOrder + 0.01 - label = "Royal Render Path from the Instance" - families = ["rendering"] + order = pyblish.api.CollectorOrder + label = "Collect Royal Render path name from the Instance" + families = ["render", "prerender", "renderlayer"] def process(self, instance): instance.data["rrPath"] = self._collect_rr_path(instance) From a4d934288590436a26ac2d9f5e8137c56d417034 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 15 Jun 2023 15:36:18 +0800 Subject: [PATCH 113/446] able to renumber frame through delivery action --- openpype/pipeline/delivery.py | 62 +++++++++++++++++++++++++++++-- openpype/plugins/load/delivery.py | 12 +++--- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 0df03bf8f8..6296ee00bf 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -120,7 +120,9 @@ def deliver_single_file( anatomy_data, format_dict, report_items, - log + log, + renumber_frame=False, + re_frame_value=0 ): """Copy single file to calculated path based on template @@ -162,8 +164,62 @@ def deliver_single_file( if not os.path.exists(delivery_folder): os.makedirs(delivery_folder) - log.debug("Copying single: {} -> {}".format(src_path, delivery_path)) - _copy_file(src_path, delivery_path) + if renumber_frame: + src_dir = os.path.dirname(src_path) + src_filepaths = os.listdir(src_dir) + src_collections, remainders = clique.assemble(src_filepaths) + + src_collection = src_collections[0] + src_indexes = list(src_collection.indexes) + # Use last frame for minimum padding + # - that should cover both 'udim' and 'frame' minimum padding + + # If the representation has `frameStart` set it renumbers the + # frame indices of the published collection. It will start from + # that `frameStart` index instead. Thus if that frame start + # differs from the collection we want to shift the destination + # frame indices from the source collection. + # In case source are published in place we need to + # skip renumbering + + destination_indexes = [ + re_frame_value + idx + for idx in range(len(src_indexes)) + ] + + # To construct the destination template with anatomy we require + # a Frame or UDIM tile set for the template data. We use the first + # index of the destination for that because that could've shifted + # from the source indexes, etc. + + # Construct destination collection from template + dst_filepaths = [] + for index in destination_indexes: + template_data = copy.deepcopy(anatomy_data) + template_data["frame"] = index + template_obj = anatomy.templates_obj["delivery"][template_name] + template_filled = template_obj.format_strict( + template_data + ) + dst_filepaths.append(template_filled) + # Make sure context contains frame + # NOTE: Frame would not be available only if template does not + # contain '{frame}' in template -> Do we want support it? + + # Update the destination indexes and padding + dst_collection = clique.assemble(dst_filepaths)[0][0] + + + for src_file_name, dst in zip(src_collection, dst_collection): + src_path = os.path.join(src_dir, src_file_name) + delivery_path = os.path.join(delivery_folder, dst) + log.debug("Copying single: {} -> {}".format( + src_path, delivery_path)) + _copy_file(src_path, delivery_path) + else: + log.debug("Copying single: {} -> {}".format(src_path, delivery_path)) + _copy_file(src_path, delivery_path) + return report_items, 1 diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 826b4ad95c..735bd4adda 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -206,6 +206,13 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self.log ] + if renumber_frame: + optional_args = [ + renumber_frame, + frame_offset + ] + args.extend(optional_args) + if repre.get("files"): src_paths = [] for repre_file in repre["files"]: @@ -228,11 +235,6 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if not frame: new_report_items, uploaded = deliver_single_file(*args) else: - optional_args = [ - renumber_frame, - frame_offset - ] - args.extend(optional_args) new_report_items, uploaded = deliver_sequence(*args) report_items.update(new_report_items) self._update_progress(uploaded) From 3cf1ffeb444be36a0831b19461e18bf935eee39f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 15 Jun 2023 15:44:08 +0800 Subject: [PATCH 114/446] hound fix --- openpype/pipeline/delivery.py | 16 ---------------- openpype/plugins/load/delivery.py | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 6296ee00bf..55043de3bb 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -171,28 +171,12 @@ def deliver_single_file( src_collection = src_collections[0] src_indexes = list(src_collection.indexes) - # Use last frame for minimum padding - # - that should cover both 'udim' and 'frame' minimum padding - - # If the representation has `frameStart` set it renumbers the - # frame indices of the published collection. It will start from - # that `frameStart` index instead. Thus if that frame start - # differs from the collection we want to shift the destination - # frame indices from the source collection. - # In case source are published in place we need to - # skip renumbering destination_indexes = [ re_frame_value + idx for idx in range(len(src_indexes)) ] - # To construct the destination template with anatomy we require - # a Frame or UDIM tile set for the template data. We use the first - # index of the destination for that because that could've shifted - # from the source indexes, etc. - - # Construct destination collection from template dst_filepaths = [] for index in destination_indexes: template_data = copy.deepcopy(anatomy_data) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 735bd4adda..79f1fd745d 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -210,7 +210,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): optional_args = [ renumber_frame, frame_offset - ] + ] args.extend(optional_args) if repre.get("files"): From 89537f2a620aab2e456a4634cd5c01d64697c6a5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 15 Jun 2023 15:44:52 +0800 Subject: [PATCH 115/446] hound fix --- openpype/pipeline/delivery.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 55043de3bb..abb666189d 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -186,14 +186,9 @@ def deliver_single_file( template_data ) dst_filepaths.append(template_filled) - # Make sure context contains frame - # NOTE: Frame would not be available only if template does not - # contain '{frame}' in template -> Do we want support it? - # Update the destination indexes and padding dst_collection = clique.assemble(dst_filepaths)[0][0] - for src_file_name, dst in zip(src_collection, dst_collection): src_path = os.path.join(src_dir, src_file_name) delivery_path = os.path.join(delivery_folder, dst) From 1e3e63aaecb5524839c06b3a74f2cfe253f82ca9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 15 Jun 2023 16:41:10 +0100 Subject: [PATCH 116/446] Implemented loader for blend representations --- .../hosts/blender/plugins/load/load_blend.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 openpype/hosts/blender/plugins/load/load_blend.py diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py new file mode 100644 index 0000000000..d35df366cf --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -0,0 +1,124 @@ +from typing import Dict, List, Optional + +import bpy + +from openpype.pipeline import ( + AVALON_CONTAINER_ID, +) +from openpype.hosts.blender.api import plugin +from openpype.hosts.blender.api.pipeline import ( + AVALON_CONTAINERS, + AVALON_PROPERTY, +) + + +class BlendLoader(plugin.AssetLoader): + """Load assets from a .blend file.""" + + families = ["model", "rig"] + representations = ["blend"] + + label = "Load Blend" + icon = "code-fork" + color = "orange" + + @staticmethod + def _get_asset_container(objects): + empties = [obj for obj in objects if obj.type == 'EMPTY'] + + for empty in empties: + if empty.get(AVALON_PROPERTY): + return empty + + return None + + def _process_data(self, libpath, group_name): + # Append all the data from the .blend file + with bpy.data.libraries.load( + libpath, link=False, relative=False + ) as (data_from, data_to): + for attr in dir(data_to): + setattr(data_to, attr, getattr(data_from, attr)) + + # Rename the object to add the asset name + for attr in dir(data_to): + for data in getattr(data_to, attr): + data.name = f"{group_name}:{data.name}" + + container = self._get_asset_container(data_to.objects) + assert container, "No asset group found" + + container.name = group_name + container.empty_display_type = 'SINGLE_ARROW' + + # Link the collection to the scene + bpy.context.scene.collection.objects.link(container) + + # Link all the container children to the collection + for obj in container.children_recursive: + bpy.context.scene.collection.objects.link(obj) + + return container + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + + asset_name = plugin.asset_name(asset, subset) + unique_number = plugin.get_unique_number(asset, subset) + group_name = plugin.asset_name(asset, subset, unique_number) + namespace = namespace or f"{asset}_{unique_number}" + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_container: + avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_container) + + container = self._process_data(libpath, group_name) + + avalon_container.objects.link(container) + + container[AVALON_PROPERTY] = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace or '', + "loader": str(self.__class__.__name__), + "representation": str(context["representation"]["_id"]), + "libpath": libpath, + "asset_name": asset_name, + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "objectName": group_name + } + + objects = [ + obj for obj in bpy.data.objects + if f"{group_name}:" in obj.name + ] + + self[:] = objects + return objects + + def exec_update(self, container: Dict, representation: Dict): + """ + Update the loaded asset. + """ + raise NotImplementedError() + + def exec_remove(self, container: Dict) -> bool: + """ + Remove an existing container from a Blender scene. + """ + raise NotImplementedError() From b0f42ae6cf5d690b8d0fbf1e93f72f933de45f0c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 15 Jun 2023 16:50:51 +0100 Subject: [PATCH 117/446] Remove library after loading --- openpype/hosts/blender/plugins/load/load_blend.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index d35df366cf..83ce4ffd3e 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -33,7 +33,7 @@ class BlendLoader(plugin.AssetLoader): return None def _process_data(self, libpath, group_name): - # Append all the data from the .blend file + # Append all the data from the .blend file with bpy.data.libraries.load( libpath, link=False, relative=False ) as (data_from, data_to): @@ -89,6 +89,10 @@ class BlendLoader(plugin.AssetLoader): avalon_container.objects.link(container) + # Remove the library from the blend file + library = bpy.data.libraries.get(bpy.path.basename(libpath)) + bpy.data.libraries.remove(library) + container[AVALON_PROPERTY] = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, From 1fe5cb0e0cfa50c1a2ec50876d168a39284ebcfd Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 16 Jun 2023 10:28:11 +0100 Subject: [PATCH 118/446] Implemented update and remove --- .../hosts/blender/plugins/load/load_blend.py | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 83ce4ffd3e..c7f916f10e 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -1,11 +1,14 @@ from typing import Dict, List, Optional +from pathlib import Path import bpy from openpype.pipeline import ( + get_representation_path, AVALON_CONTAINER_ID, ) from openpype.hosts.blender.api import plugin +from openpype.hosts.blender.api.lib import imprint from openpype.hosts.blender.api.pipeline import ( AVALON_CONTAINERS, AVALON_PROPERTY, @@ -93,7 +96,7 @@ class BlendLoader(plugin.AssetLoader): library = bpy.data.libraries.get(bpy.path.basename(libpath)) bpy.data.libraries.remove(library) - container[AVALON_PROPERTY] = { + data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": name, @@ -107,9 +110,11 @@ class BlendLoader(plugin.AssetLoader): "objectName": group_name } + container[AVALON_PROPERTY] = data + objects = [ obj for obj in bpy.data.objects - if f"{group_name}:" in obj.name + if obj.name.startswith(f"{group_name}:") ] self[:] = objects @@ -119,10 +124,58 @@ class BlendLoader(plugin.AssetLoader): """ Update the loaded asset. """ - raise NotImplementedError() + group_name = container["objectName"] + asset_group = bpy.data.objects.get(group_name) + libpath = Path(get_representation_path(representation)).as_posix() + + assert asset_group, ( + f"The asset is not loaded: {container['objectName']}" + ) + + transform = asset_group.matrix_basis.copy() + old_data = dict(asset_group.get(AVALON_PROPERTY)) + + self.exec_remove(container) + + asset_group = self._process_data(libpath, group_name) + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + avalon_container.objects.link(asset_group) + + # Remove the library from the blend file + library = bpy.data.libraries.get(bpy.path.basename(libpath)) + bpy.data.libraries.remove(library) + + asset_group.matrix_basis = transform + + asset_group[AVALON_PROPERTY] = old_data + + new_data = { + "libpath": libpath, + "representation": str(representation["_id"]), + "parent": str(representation["parent"]), + } + + imprint(asset_group, new_data) def exec_remove(self, container: Dict) -> bool: """ Remove an existing container from a Blender scene. """ - raise NotImplementedError() + group_name = container["objectName"] + asset_group = bpy.data.objects.get(group_name) + + attrs = [ + attr for attr in dir(bpy.data) + if isinstance( + getattr(bpy.data, attr), + bpy.types.bpy_prop_collection + ) + ] + + for attr in attrs: + for data in getattr(bpy.data, attr): + if data.name.startswith(f"{group_name}:"): + getattr(bpy.data, attr).remove(data) + + bpy.data.objects.remove(asset_group) From b06a7108b64246dc022f3a4472b896d53e2311aa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 16 Jun 2023 22:22:08 +0800 Subject: [PATCH 119/446] add collector to tray publisher for getting frame range data --- .../publish/collect_anatomy_frame_range.py | 33 +++++++++++++++++++ .../project_settings/traypublisher.json | 5 +++ .../schema_project_traypublisher.json | 4 +++ 3 files changed, 42 insertions(+) create mode 100644 openpype/plugins/publish/collect_anatomy_frame_range.py diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_anatomy_frame_range.py new file mode 100644 index 0000000000..71a5dcfeb0 --- /dev/null +++ b/openpype/plugins/publish/collect_anatomy_frame_range.py @@ -0,0 +1,33 @@ +import pyblish.api + + +class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): + """Collect Frame Range specific Anatomy data. + + Plugin is running for all instances on context even not active instances. + """ + + order = pyblish.api.CollectorOrder + 0.491 + label = "Collect Anatomy Frame Range" + hosts = ["traypublisher"] + + def process(self, instance): + self.log.info("Collecting Anatomy frame range.") + asset_doc = instance.data.get("assetEntity") + if not asset_doc: + self.log.info("Missing required data..") + return + + asset_data = asset_doc["data"] + for key in ( + "fps", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd" + ): + if key not in instance.data and key in asset_data: + value = asset_data[key] + instance.data[key] = value + + self.log.info("Anatomy frame range collection finished.") diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 3a42c93515..4ad492c77b 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -318,6 +318,11 @@ } }, "publish": { + "CollectAnatomyFrameRange": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateFrameRange": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 3703d82856..44442a07d4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -343,6 +343,10 @@ "type": "schema_template", "name": "template_validate_plugin", "template_data": [ + { + "key": "CollectAnatomyFrameRange", + "label": "Collect Anatomy frame range" + }, { "key": "ValidateFrameRange", "label": "Validate frame range" From d6aabc93ddb485bda730d251dd64a70ed57899dc Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 16 Jun 2023 15:29:09 +0100 Subject: [PATCH 120/446] Added support for layouts in new blend loader --- .../hosts/blender/plugins/load/load_blend.py | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index c7f916f10e..b64793566f 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -4,9 +4,11 @@ from pathlib import Path import bpy from openpype.pipeline import ( + legacy_create, get_representation_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.create import get_legacy_creator_by_name from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.lib import imprint from openpype.hosts.blender.api.pipeline import ( @@ -18,7 +20,7 @@ from openpype.hosts.blender.api.pipeline import ( class BlendLoader(plugin.AssetLoader): """Load assets from a .blend file.""" - families = ["model", "rig"] + families = ["model", "rig", "layout"] representations = ["blend"] label = "Load Blend" @@ -35,6 +37,31 @@ class BlendLoader(plugin.AssetLoader): return None + def _post_process_layout(self, container, asset, representation): + rigs = [ + obj for obj in container.children_recursive + if ( + obj.type == 'EMPTY' and + obj.get(AVALON_PROPERTY) and + obj.get(AVALON_PROPERTY).get('family') == 'rig' + ) + ] + + for rig in rigs: + creator_plugin = get_legacy_creator_by_name("CreateAnimation") + legacy_create( + creator_plugin, + name=rig.name.split(':')[-1] + "_animation", + asset=asset, + options={ + "useSelection": False, + "asset_group": rig + }, + data={ + "dependencies": representation + } + ) + def _process_data(self, libpath, group_name): # Append all the data from the .blend file with bpy.data.libraries.load( @@ -61,6 +88,10 @@ class BlendLoader(plugin.AssetLoader): for obj in container.children_recursive: bpy.context.scene.collection.objects.link(obj) + # Remove the library from the blend file + library = bpy.data.libraries.get(bpy.path.basename(libpath)) + bpy.data.libraries.remove(library) + return container def process_asset( @@ -78,6 +109,13 @@ class BlendLoader(plugin.AssetLoader): asset = context["asset"]["name"] subset = context["subset"]["name"] + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "model" + + representation = str(context["representation"]["_id"]) + asset_name = plugin.asset_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) group_name = plugin.asset_name(asset, subset, unique_number) @@ -90,11 +128,10 @@ class BlendLoader(plugin.AssetLoader): container = self._process_data(libpath, group_name) - avalon_container.objects.link(container) + if family == "layout": + self._post_process_layout(container, asset, representation) - # Remove the library from the blend file - library = bpy.data.libraries.get(bpy.path.basename(libpath)) - bpy.data.libraries.remove(library) + avalon_container.objects.link(container) data = { "schema": "openpype:container-2.0", @@ -134,6 +171,7 @@ class BlendLoader(plugin.AssetLoader): transform = asset_group.matrix_basis.copy() old_data = dict(asset_group.get(AVALON_PROPERTY)) + parent = asset_group.parent self.exec_remove(container) @@ -142,11 +180,8 @@ class BlendLoader(plugin.AssetLoader): avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.objects.link(asset_group) - # Remove the library from the blend file - library = bpy.data.libraries.get(bpy.path.basename(libpath)) - bpy.data.libraries.remove(library) - asset_group.matrix_basis = transform + asset_group.parent = parent asset_group[AVALON_PROPERTY] = old_data From e41621875f9b01a0f4aa7c5ad92ebd85b3ad0098 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 16 Jun 2023 15:32:55 +0100 Subject: [PATCH 121/446] Removed old loaders and redundant plugins --- .../plugins/create/create_blender_scene.py | 37 -- .../blender/plugins/load/load_layout_blend.py | 469 ------------------ .../hosts/blender/plugins/load/load_model.py | 296 ----------- .../hosts/blender/plugins/load/load_rig.py | 417 ---------------- .../publish/extract_blender_scene_raw.py | 49 -- openpype/plugins/publish/integrate_legacy.py | 3 +- 6 files changed, 1 insertion(+), 1270 deletions(-) delete mode 100644 openpype/hosts/blender/plugins/create/create_blender_scene.py delete mode 100644 openpype/hosts/blender/plugins/load/load_layout_blend.py delete mode 100644 openpype/hosts/blender/plugins/load/load_model.py delete mode 100644 openpype/hosts/blender/plugins/load/load_rig.py delete mode 100644 openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py diff --git a/openpype/hosts/blender/plugins/create/create_blender_scene.py b/openpype/hosts/blender/plugins/create/create_blender_scene.py deleted file mode 100644 index b63ed4fd3f..0000000000 --- a/openpype/hosts/blender/plugins/create/create_blender_scene.py +++ /dev/null @@ -1,37 +0,0 @@ -import bpy - -from openpype.pipeline import legacy_io -from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES - - -class CreateBlenderScene(plugin.Creator): - """Raw Blender Scene file export""" - - name = "blenderScene" - label = "Blender Scene" - family = "blenderScene" - icon = "file-archive-o" - - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) - ops.execute_in_main_thread(mti) - - def _process(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - self.data['task'] = legacy_io.Session.get('AVALON_TASK') - lib.imprint(asset_group, self.data) - - return asset_group diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py deleted file mode 100644 index 7d2fd23444..0000000000 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ /dev/null @@ -1,469 +0,0 @@ -"""Load a layout in Blender.""" - -from pathlib import Path -from pprint import pformat -from typing import Dict, List, Optional - -import bpy - -from openpype.pipeline import ( - legacy_create, - get_representation_path, - AVALON_CONTAINER_ID, -) -from openpype.pipeline.create import get_legacy_creator_by_name -from openpype.hosts.blender.api import plugin -from openpype.hosts.blender.api.pipeline import ( - AVALON_CONTAINERS, - AVALON_PROPERTY, -) - - -class BlendLayoutLoader(plugin.AssetLoader): - """Load layout from a .blend file.""" - - families = ["layout"] - representations = ["blend"] - - label = "Link Layout" - icon = "code-fork" - color = "orange" - - def _remove(self, asset_group): - objects = list(asset_group.children) - - for obj in objects: - if obj.type == 'MESH': - for material_slot in list(obj.material_slots): - if material_slot.material: - bpy.data.materials.remove(material_slot.material) - bpy.data.meshes.remove(obj.data) - elif obj.type == 'ARMATURE': - objects.extend(obj.children) - bpy.data.armatures.remove(obj.data) - elif obj.type == 'CURVE': - bpy.data.curves.remove(obj.data) - elif obj.type == 'EMPTY': - objects.extend(obj.children) - bpy.data.objects.remove(obj) - - def _remove_asset_and_library(self, asset_group): - if not asset_group.get(AVALON_PROPERTY): - return - - libpath = asset_group.get(AVALON_PROPERTY).get('libpath') - - if not libpath: - return - - # Check how many assets use the same library - count = 0 - for obj in bpy.data.collections.get(AVALON_CONTAINERS).all_objects: - if obj.get(AVALON_PROPERTY).get('libpath') == libpath: - count += 1 - - self._remove(asset_group) - - bpy.data.objects.remove(asset_group) - - # If it is the last object to use that library, remove it - if count == 1: - library = bpy.data.libraries.get(bpy.path.basename(libpath)) - if library: - bpy.data.libraries.remove(library) - - def _process( - self, libpath, asset_group, group_name, asset, representation, - actions, anim_instances - ): - with bpy.data.libraries.load( - libpath, link=True, relative=False - ) as (data_from, data_to): - data_to.objects = data_from.objects - - parent = bpy.context.scene.collection - - empties = [obj for obj in data_to.objects if obj.type == 'EMPTY'] - - container = None - - for empty in empties: - if (empty.get(AVALON_PROPERTY) and - empty.get(AVALON_PROPERTY).get('family') == 'layout'): - container = empty - break - - assert container, "No asset group found" - - # Children must be linked before parents, - # otherwise the hierarchy will break - objects = [] - nodes = list(container.children) - - allowed_types = ['ARMATURE', 'MESH', 'EMPTY'] - - for obj in nodes: - if obj.type in allowed_types: - obj.parent = asset_group - - for obj in nodes: - if obj.type in allowed_types: - objects.append(obj) - nodes.extend(list(obj.children)) - - objects.reverse() - - constraints = [] - - armatures = [obj for obj in objects if obj.type == 'ARMATURE'] - - for armature in armatures: - for bone in armature.pose.bones: - for constraint in bone.constraints: - if hasattr(constraint, 'target'): - constraints.append(constraint) - - for obj in objects: - parent.objects.link(obj) - - for obj in objects: - local_obj = plugin.prepare_data(obj) - - action = None - - if actions: - action = actions.get(local_obj.name, None) - - if local_obj.type == 'MESH': - plugin.prepare_data(local_obj.data) - - if obj != local_obj: - for constraint in constraints: - if constraint.target == obj: - constraint.target = local_obj - - for material_slot in local_obj.material_slots: - if material_slot.material: - plugin.prepare_data(material_slot.material) - elif local_obj.type == 'ARMATURE': - plugin.prepare_data(local_obj.data) - - if action: - if local_obj.animation_data is None: - local_obj.animation_data_create() - local_obj.animation_data.action = action - elif (local_obj.animation_data and - local_obj.animation_data.action): - plugin.prepare_data( - local_obj.animation_data.action) - - # Set link the drivers to the local object - if local_obj.data.animation_data: - for d in local_obj.data.animation_data.drivers: - for v in d.driver.variables: - for t in v.targets: - t.id = local_obj - - elif local_obj.type == 'EMPTY': - if (not anim_instances or - (anim_instances and - local_obj.name not in anim_instances.keys())): - avalon = local_obj.get(AVALON_PROPERTY) - if avalon and avalon.get('family') == 'rig': - creator_plugin = get_legacy_creator_by_name( - "CreateAnimation") - if not creator_plugin: - raise ValueError( - "Creator plugin \"CreateAnimation\" was " - "not found.") - - legacy_create( - creator_plugin, - name=local_obj.name.split(':')[-1] + "_animation", - asset=asset, - options={"useSelection": False, - "asset_group": local_obj}, - data={"dependencies": representation} - ) - - if not local_obj.get(AVALON_PROPERTY): - local_obj[AVALON_PROPERTY] = dict() - - avalon_info = local_obj[AVALON_PROPERTY] - avalon_info.update({"container_name": group_name}) - - objects.reverse() - - armatures = [ - obj for obj in bpy.data.objects - if obj.type == 'ARMATURE' and obj.library is None] - arm_act = {} - - # The armatures with an animation need to be at the center of the - # scene to be hooked correctly by the curves modifiers. - for armature in armatures: - if armature.animation_data and armature.animation_data.action: - arm_act[armature] = armature.animation_data.action - armature.animation_data.action = None - armature.location = (0.0, 0.0, 0.0) - for bone in armature.pose.bones: - bone.location = (0.0, 0.0, 0.0) - bone.rotation_euler = (0.0, 0.0, 0.0) - - curves = [obj for obj in data_to.objects if obj.type == 'CURVE'] - - for curve in curves: - curve_name = curve.name.split(':')[0] - curve_obj = bpy.data.objects.get(curve_name) - - local_obj = plugin.prepare_data(curve) - plugin.prepare_data(local_obj.data) - - # Curves need to reset the hook, but to do that they need to be - # in the view layer. - parent.objects.link(local_obj) - plugin.deselect_all() - local_obj.select_set(True) - bpy.context.view_layer.objects.active = local_obj - if local_obj.library is None: - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.object.hook_reset() - bpy.ops.object.mode_set(mode='OBJECT') - parent.objects.unlink(local_obj) - - local_obj.use_fake_user = True - - for mod in local_obj.modifiers: - mod.object = bpy.data.objects.get(f"{mod.object.name}") - - if not local_obj.get(AVALON_PROPERTY): - local_obj[AVALON_PROPERTY] = dict() - - avalon_info = local_obj[AVALON_PROPERTY] - avalon_info.update({"container_name": group_name}) - - local_obj.parent = curve_obj - objects.append(local_obj) - - for armature in armatures: - if arm_act.get(armature): - armature.animation_data.action = arm_act[armature] - - while bpy.data.orphans_purge(do_local_ids=False): - pass - - plugin.deselect_all() - - return objects - - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - representation = str(context["representation"]["_id"]) - - asset_name = plugin.asset_name(asset, subset) - unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) - namespace = namespace or f"{asset}_{unique_number}" - - avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) - if not avalon_container: - avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) - bpy.context.scene.collection.children.link(avalon_container) - - asset_group = bpy.data.objects.new(group_name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - avalon_container.objects.link(asset_group) - - objects = self._process( - libpath, asset_group, group_name, asset, representation, - None, None) - - for child in asset_group.children: - if child.get(AVALON_PROPERTY): - avalon_container.objects.link(child) - - bpy.context.scene.collection.objects.link(asset_group) - - asset_group[AVALON_PROPERTY] = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, - "name": name, - "namespace": namespace or '', - "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), - "libpath": libpath, - "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "family": context["representation"]["context"]["family"], - "objectName": group_name - } - - self[:] = objects - return objects - - def update(self, container: Dict, representation: Dict): - """Update the loaded asset. - - This will remove all objects of the current collection, load the new - ones and add them to the collection. - If the objects of the collection are used in another collection they - will not be removed, only unlinked. Normally this should not be the - case though. - - Warning: - No nested collections are supported at the moment! - """ - object_name = container["objectName"] - asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(representation)) - extension = libpath.suffix.lower() - - self.log.info( - "Container: %s\nRepresentation: %s", - pformat(container, indent=2), - pformat(representation, indent=2), - ) - - assert asset_group, ( - f"The asset is not loaded: {container['objectName']}" - ) - assert libpath, ( - "No existing library file found for {container['objectName']}" - ) - assert libpath.is_file(), ( - f"The file doesn't exist: {libpath}" - ) - assert extension in plugin.VALID_EXTENSIONS, ( - f"Unsupported file: {libpath}" - ) - - metadata = asset_group.get(AVALON_PROPERTY) - group_libpath = metadata["libpath"] - - normalized_group_libpath = ( - str(Path(bpy.path.abspath(group_libpath)).resolve()) - ) - normalized_libpath = ( - str(Path(bpy.path.abspath(str(libpath))).resolve()) - ) - self.log.debug( - "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", - normalized_group_libpath, - normalized_libpath, - ) - if normalized_group_libpath == normalized_libpath: - self.log.info("Library already loaded, not updating...") - return - - actions = {} - anim_instances = {} - - for obj in asset_group.children: - obj_meta = obj.get(AVALON_PROPERTY) - if obj_meta.get('family') == 'rig': - # Get animation instance - collections = list(obj.users_collection) - for c in collections: - avalon = c.get(AVALON_PROPERTY) - if avalon and avalon.get('family') == 'animation': - anim_instances[obj.name] = c.name - break - - # Get armature's action - rig = None - for child in obj.children: - if child.type == 'ARMATURE': - rig = child - break - if not rig: - raise Exception("No armature in the rig asset group.") - if rig.animation_data and rig.animation_data.action: - instance_name = obj_meta.get('instance_name') - actions[instance_name] = rig.animation_data.action - - mat = asset_group.matrix_basis.copy() - - # Remove the children of the asset_group first - for child in list(asset_group.children): - self._remove_asset_and_library(child) - - # Check how many assets use the same library - count = 0 - for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: - if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath: - count += 1 - - self._remove(asset_group) - - # If it is the last object to use that library, remove it - if count == 1: - library = bpy.data.libraries.get(bpy.path.basename(group_libpath)) - if library: - bpy.data.libraries.remove(library) - - asset = container.get("asset_name").split("_")[0] - - self._process( - str(libpath), asset_group, object_name, asset, - str(representation.get("_id")), actions, anim_instances - ) - - # Link the new objects to the animation collection - for inst in anim_instances.keys(): - try: - obj = bpy.data.objects[inst] - bpy.data.collections[anim_instances[inst]].objects.link(obj) - except KeyError: - self.log.info(f"Object {inst} does not exist anymore.") - coll = bpy.data.collections.get(anim_instances[inst]) - if (coll): - bpy.data.collections.remove(coll) - - avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) - for child in asset_group.children: - if child.get(AVALON_PROPERTY): - avalon_container.objects.link(child) - - asset_group.matrix_basis = mat - - metadata["libpath"] = str(libpath) - metadata["representation"] = str(representation["_id"]) - - def exec_remove(self, container: Dict) -> bool: - """Remove an existing container from a Blender scene. - - Arguments: - container (openpype:container-1.0): Container to remove, - from `host.ls()`. - - Returns: - bool: Whether the container was deleted. - - Warning: - No nested collections are supported at the moment! - """ - object_name = container["objectName"] - asset_group = bpy.data.objects.get(object_name) - - if not asset_group: - return False - - # Remove the children of the asset_group first - for child in list(asset_group.children): - self._remove_asset_and_library(child) - - self._remove_asset_and_library(asset_group) - - return True diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py deleted file mode 100644 index 0a5d98ffa0..0000000000 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ /dev/null @@ -1,296 +0,0 @@ -"""Load a model asset in Blender.""" - -from pathlib import Path -from pprint import pformat -from typing import Dict, List, Optional - -import bpy - -from openpype.pipeline import ( - get_representation_path, - AVALON_CONTAINER_ID, -) -from openpype.hosts.blender.api import plugin -from openpype.hosts.blender.api.pipeline import ( - AVALON_CONTAINERS, - AVALON_PROPERTY, -) - - -class BlendModelLoader(plugin.AssetLoader): - """Load models from a .blend file. - - Because they come from a .blend file we can simply link the collection that - contains the model. There is no further need to 'containerise' it. - """ - - families = ["model"] - representations = ["blend"] - - label = "Link Model" - icon = "code-fork" - color = "orange" - - def _remove(self, asset_group): - objects = list(asset_group.children) - - for obj in objects: - if obj.type == 'MESH': - for material_slot in list(obj.material_slots): - bpy.data.materials.remove(material_slot.material) - bpy.data.meshes.remove(obj.data) - elif obj.type == 'EMPTY': - objects.extend(obj.children) - bpy.data.objects.remove(obj) - - def _process(self, libpath, asset_group, group_name): - with bpy.data.libraries.load( - libpath, link=True, relative=False - ) as (data_from, data_to): - data_to.objects = data_from.objects - - parent = bpy.context.scene.collection - - empties = [obj for obj in data_to.objects if obj.type == 'EMPTY'] - - container = None - - for empty in empties: - if empty.get(AVALON_PROPERTY): - container = empty - break - - assert container, "No asset group found" - - # Children must be linked before parents, - # otherwise the hierarchy will break - objects = [] - nodes = list(container.children) - - for obj in nodes: - obj.parent = asset_group - - for obj in nodes: - objects.append(obj) - nodes.extend(list(obj.children)) - - objects.reverse() - - for obj in objects: - parent.objects.link(obj) - - for obj in objects: - local_obj = plugin.prepare_data(obj, group_name) - if local_obj.type != 'EMPTY': - plugin.prepare_data(local_obj.data, group_name) - - for material_slot in local_obj.material_slots: - if material_slot.material: - plugin.prepare_data(material_slot.material, group_name) - - if not local_obj.get(AVALON_PROPERTY): - local_obj[AVALON_PROPERTY] = dict() - - avalon_info = local_obj[AVALON_PROPERTY] - avalon_info.update({"container_name": group_name}) - - objects.reverse() - - bpy.data.orphans_purge(do_local_ids=False) - - plugin.deselect_all() - - return objects - - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - - asset_name = plugin.asset_name(asset, subset) - unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) - namespace = namespace or f"{asset}_{unique_number}" - - avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) - if not avalon_container: - avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) - bpy.context.scene.collection.children.link(avalon_container) - - asset_group = bpy.data.objects.new(group_name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - avalon_container.objects.link(asset_group) - - plugin.deselect_all() - - if options is not None: - parent = options.get('parent') - transform = options.get('transform') - - if parent and transform: - location = transform.get('translation') - rotation = transform.get('rotation') - scale = transform.get('scale') - - asset_group.location = ( - location.get('x'), - location.get('y'), - location.get('z') - ) - asset_group.rotation_euler = ( - rotation.get('x'), - rotation.get('y'), - rotation.get('z') - ) - asset_group.scale = ( - scale.get('x'), - scale.get('y'), - scale.get('z') - ) - - bpy.context.view_layer.objects.active = parent - asset_group.select_set(True) - - bpy.ops.object.parent_set(keep_transform=True) - - plugin.deselect_all() - - objects = self._process(libpath, asset_group, group_name) - - bpy.context.scene.collection.objects.link(asset_group) - - asset_group[AVALON_PROPERTY] = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, - "name": name, - "namespace": namespace or '', - "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), - "libpath": libpath, - "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "family": context["representation"]["context"]["family"], - "objectName": group_name - } - - self[:] = objects - return objects - - def exec_update(self, container: Dict, representation: Dict): - """Update the loaded asset. - - This will remove all objects of the current collection, load the new - ones and add them to the collection. - If the objects of the collection are used in another collection they - will not be removed, only unlinked. Normally this should not be the - case though. - """ - object_name = container["objectName"] - asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(representation)) - extension = libpath.suffix.lower() - - self.log.info( - "Container: %s\nRepresentation: %s", - pformat(container, indent=2), - pformat(representation, indent=2), - ) - - assert asset_group, ( - f"The asset is not loaded: {container['objectName']}" - ) - assert libpath, ( - "No existing library file found for {container['objectName']}" - ) - assert libpath.is_file(), ( - f"The file doesn't exist: {libpath}" - ) - assert extension in plugin.VALID_EXTENSIONS, ( - f"Unsupported file: {libpath}" - ) - - metadata = asset_group.get(AVALON_PROPERTY) - group_libpath = metadata["libpath"] - - normalized_group_libpath = ( - str(Path(bpy.path.abspath(group_libpath)).resolve()) - ) - normalized_libpath = ( - str(Path(bpy.path.abspath(str(libpath))).resolve()) - ) - self.log.debug( - "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", - normalized_group_libpath, - normalized_libpath, - ) - if normalized_group_libpath == normalized_libpath: - self.log.info("Library already loaded, not updating...") - return - - # Check how many assets use the same library - count = 0 - for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: - if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath: - count += 1 - - mat = asset_group.matrix_basis.copy() - - self._remove(asset_group) - - # If it is the last object to use that library, remove it - if count == 1: - library = bpy.data.libraries.get(bpy.path.basename(group_libpath)) - if library: - bpy.data.libraries.remove(library) - - self._process(str(libpath), asset_group, object_name) - - asset_group.matrix_basis = mat - - metadata["libpath"] = str(libpath) - metadata["representation"] = str(representation["_id"]) - metadata["parent"] = str(representation["parent"]) - - def exec_remove(self, container: Dict) -> bool: - """Remove an existing container from a Blender scene. - - Arguments: - container (openpype:container-1.0): Container to remove, - from `host.ls()`. - - Returns: - bool: Whether the container was deleted. - """ - object_name = container["objectName"] - asset_group = bpy.data.objects.get(object_name) - libpath = asset_group.get(AVALON_PROPERTY).get('libpath') - - # Check how many assets use the same library - count = 0 - for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: - if obj.get(AVALON_PROPERTY).get('libpath') == libpath: - count += 1 - - if not asset_group: - return False - - self._remove(asset_group) - - bpy.data.objects.remove(asset_group) - - # If it is the last object to use that library, remove it - if count == 1: - library = bpy.data.libraries.get(bpy.path.basename(libpath)) - bpy.data.libraries.remove(library) - - return True diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py deleted file mode 100644 index 1d23a70061..0000000000 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ /dev/null @@ -1,417 +0,0 @@ -"""Load a rig asset in Blender.""" - -from pathlib import Path -from pprint import pformat -from typing import Dict, List, Optional - -import bpy - -from openpype.pipeline import ( - legacy_create, - get_representation_path, - AVALON_CONTAINER_ID, -) -from openpype.pipeline.create import get_legacy_creator_by_name -from openpype.hosts.blender.api import ( - plugin, - get_selection, -) -from openpype.hosts.blender.api.pipeline import ( - AVALON_CONTAINERS, - AVALON_PROPERTY, -) - - -class BlendRigLoader(plugin.AssetLoader): - """Load rigs from a .blend file.""" - - families = ["rig"] - representations = ["blend"] - - label = "Link Rig" - icon = "code-fork" - color = "orange" - - def _remove(self, asset_group): - objects = list(asset_group.children) - - for obj in objects: - if obj.type == 'MESH': - for material_slot in list(obj.material_slots): - if material_slot.material: - bpy.data.materials.remove(material_slot.material) - bpy.data.meshes.remove(obj.data) - elif obj.type == 'ARMATURE': - objects.extend(obj.children) - bpy.data.armatures.remove(obj.data) - elif obj.type == 'CURVE': - bpy.data.curves.remove(obj.data) - elif obj.type == 'EMPTY': - objects.extend(obj.children) - bpy.data.objects.remove(obj) - - def _process(self, libpath, asset_group, group_name, action): - with bpy.data.libraries.load( - libpath, link=True, relative=False - ) as (data_from, data_to): - data_to.objects = data_from.objects - - parent = bpy.context.scene.collection - - empties = [obj for obj in data_to.objects if obj.type == 'EMPTY'] - - container = None - - for empty in empties: - if empty.get(AVALON_PROPERTY): - container = empty - break - - assert container, "No asset group found" - - # Children must be linked before parents, - # otherwise the hierarchy will break - objects = [] - nodes = list(container.children) - - allowed_types = ['ARMATURE', 'MESH'] - - for obj in nodes: - if obj.type in allowed_types: - obj.parent = asset_group - - for obj in nodes: - if obj.type in allowed_types: - objects.append(obj) - nodes.extend(list(obj.children)) - - objects.reverse() - - constraints = [] - - armatures = [obj for obj in objects if obj.type == 'ARMATURE'] - - for armature in armatures: - for bone in armature.pose.bones: - for constraint in bone.constraints: - if hasattr(constraint, 'target'): - constraints.append(constraint) - - for obj in objects: - parent.objects.link(obj) - - for obj in objects: - local_obj = plugin.prepare_data(obj, group_name) - - if local_obj.type == 'MESH': - plugin.prepare_data(local_obj.data, group_name) - - if obj != local_obj: - for constraint in constraints: - if constraint.target == obj: - constraint.target = local_obj - - for material_slot in local_obj.material_slots: - if material_slot.material: - plugin.prepare_data(material_slot.material, group_name) - elif local_obj.type == 'ARMATURE': - plugin.prepare_data(local_obj.data, group_name) - - if action is not None: - if local_obj.animation_data is None: - local_obj.animation_data_create() - local_obj.animation_data.action = action - elif (local_obj.animation_data and - local_obj.animation_data.action is not None): - plugin.prepare_data( - local_obj.animation_data.action, group_name) - - # Set link the drivers to the local object - if local_obj.data.animation_data: - for d in local_obj.data.animation_data.drivers: - for v in d.driver.variables: - for t in v.targets: - t.id = local_obj - - if not local_obj.get(AVALON_PROPERTY): - local_obj[AVALON_PROPERTY] = dict() - - avalon_info = local_obj[AVALON_PROPERTY] - avalon_info.update({"container_name": group_name}) - - objects.reverse() - - curves = [obj for obj in data_to.objects if obj.type == 'CURVE'] - - for curve in curves: - local_obj = plugin.prepare_data(curve, group_name) - plugin.prepare_data(local_obj.data, group_name) - - local_obj.use_fake_user = True - - for mod in local_obj.modifiers: - mod_target_name = mod.object.name - mod.object = bpy.data.objects.get( - f"{group_name}:{mod_target_name}") - - if not local_obj.get(AVALON_PROPERTY): - local_obj[AVALON_PROPERTY] = dict() - - avalon_info = local_obj[AVALON_PROPERTY] - avalon_info.update({"container_name": group_name}) - - local_obj.parent = asset_group - objects.append(local_obj) - - while bpy.data.orphans_purge(do_local_ids=False): - pass - - plugin.deselect_all() - - return objects - - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - - asset_name = plugin.asset_name(asset, subset) - unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) - namespace = namespace or f"{asset}_{unique_number}" - - avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) - if not avalon_container: - avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) - bpy.context.scene.collection.children.link(avalon_container) - - asset_group = bpy.data.objects.new(group_name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - avalon_container.objects.link(asset_group) - - action = None - - plugin.deselect_all() - - create_animation = False - anim_file = None - - if options is not None: - parent = options.get('parent') - transform = options.get('transform') - action = options.get('action') - create_animation = options.get('create_animation') - anim_file = options.get('animation_file') - - if parent and transform: - location = transform.get('translation') - rotation = transform.get('rotation') - scale = transform.get('scale') - - asset_group.location = ( - location.get('x'), - location.get('y'), - location.get('z') - ) - asset_group.rotation_euler = ( - rotation.get('x'), - rotation.get('y'), - rotation.get('z') - ) - asset_group.scale = ( - scale.get('x'), - scale.get('y'), - scale.get('z') - ) - - bpy.context.view_layer.objects.active = parent - asset_group.select_set(True) - - bpy.ops.object.parent_set(keep_transform=True) - - plugin.deselect_all() - - objects = self._process(libpath, asset_group, group_name, action) - - if create_animation: - creator_plugin = get_legacy_creator_by_name("CreateAnimation") - if not creator_plugin: - raise ValueError("Creator plugin \"CreateAnimation\" was " - "not found.") - - asset_group.select_set(True) - - animation_asset = options.get('animation_asset') - - legacy_create( - creator_plugin, - name=namespace + "_animation", - # name=f"{unique_number}_{subset}_animation", - asset=animation_asset, - options={"useSelection": False, "asset_group": asset_group}, - data={"dependencies": str(context["representation"]["_id"])} - ) - - plugin.deselect_all() - - if anim_file: - bpy.ops.import_scene.fbx(filepath=anim_file, anim_offset=0.0) - - imported = get_selection() - - armature = [ - o for o in asset_group.children if o.type == 'ARMATURE'][0] - - imported_group = [ - o for o in imported if o.type == 'EMPTY'][0] - - for obj in imported: - if obj.type == 'ARMATURE': - if not armature.animation_data: - armature.animation_data_create() - armature.animation_data.action = obj.animation_data.action - - self._remove(imported_group) - bpy.data.objects.remove(imported_group) - - bpy.context.scene.collection.objects.link(asset_group) - - asset_group[AVALON_PROPERTY] = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, - "name": name, - "namespace": namespace or '', - "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), - "libpath": libpath, - "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "family": context["representation"]["context"]["family"], - "objectName": group_name - } - - self[:] = objects - return objects - - def exec_update(self, container: Dict, representation: Dict): - """Update the loaded asset. - - This will remove all children of the asset group, load the new ones - and add them as children of the group. - """ - object_name = container["objectName"] - asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(representation)) - extension = libpath.suffix.lower() - - self.log.info( - "Container: %s\nRepresentation: %s", - pformat(container, indent=2), - pformat(representation, indent=2), - ) - - assert asset_group, ( - f"The asset is not loaded: {container['objectName']}" - ) - assert libpath, ( - "No existing library file found for {container['objectName']}" - ) - assert libpath.is_file(), ( - f"The file doesn't exist: {libpath}" - ) - assert extension in plugin.VALID_EXTENSIONS, ( - f"Unsupported file: {libpath}" - ) - - metadata = asset_group.get(AVALON_PROPERTY) - group_libpath = metadata["libpath"] - - normalized_group_libpath = ( - str(Path(bpy.path.abspath(group_libpath)).resolve()) - ) - normalized_libpath = ( - str(Path(bpy.path.abspath(str(libpath))).resolve()) - ) - self.log.debug( - "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", - normalized_group_libpath, - normalized_libpath, - ) - if normalized_group_libpath == normalized_libpath: - self.log.info("Library already loaded, not updating...") - return - - # Check how many assets use the same library - count = 0 - for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: - if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath: - count += 1 - - # Get the armature of the rig - objects = asset_group.children - armature = [obj for obj in objects if obj.type == 'ARMATURE'][0] - - action = None - if armature.animation_data and armature.animation_data.action: - action = armature.animation_data.action - - mat = asset_group.matrix_basis.copy() - - self._remove(asset_group) - - # If it is the last object to use that library, remove it - if count == 1: - library = bpy.data.libraries.get(bpy.path.basename(group_libpath)) - bpy.data.libraries.remove(library) - - self._process(str(libpath), asset_group, object_name, action) - - asset_group.matrix_basis = mat - - metadata["libpath"] = str(libpath) - metadata["representation"] = str(representation["_id"]) - metadata["parent"] = str(representation["parent"]) - - def exec_remove(self, container: Dict) -> bool: - """Remove an existing asset group from a Blender scene. - - Arguments: - container (openpype:container-1.0): Container to remove, - from `host.ls()`. - - Returns: - bool: Whether the asset group was deleted. - """ - object_name = container["objectName"] - asset_group = bpy.data.objects.get(object_name) - libpath = asset_group.get(AVALON_PROPERTY).get('libpath') - - # Check how many assets use the same library - count = 0 - for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: - if obj.get(AVALON_PROPERTY).get('libpath') == libpath: - count += 1 - - if not asset_group: - return False - - self._remove(asset_group) - - bpy.data.objects.remove(asset_group) - - # If it is the last object to use that library, remove it - if count == 1: - library = bpy.data.libraries.get(bpy.path.basename(libpath)) - bpy.data.libraries.remove(library) - - return True diff --git a/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py b/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py deleted file mode 100644 index b3f6f6460c..0000000000 --- a/openpype/hosts/blender/plugins/publish/extract_blender_scene_raw.py +++ /dev/null @@ -1,49 +0,0 @@ -import os - -import bpy - -from openpype.pipeline import publish - - -class ExtractBlenderSceneRaw(publish.Extractor): - """Extract as Blender Scene (raw). - - This will preserve all references, construction history, etc. - """ - - label = "Blender Scene (Raw)" - hosts = ["blender"] - families = ["blenderScene", "layout"] - scene_type = "blend" - - def process(self, instance): - # Define extract output file path - dir_path = self.staging_dir(instance) - filename = "{0}.{1}".format(instance.name, self.scene_type) - path = os.path.join(dir_path, filename) - - # We need to get all the data blocks for all the blender objects. - # The following set will contain all the data blocks from version - # 2.93 of Blender. - data_blocks = set() - - for attr in dir(bpy.data): - data_block = getattr(bpy.data, attr) - if isinstance(data_block, bpy.types.bpy_prop_collection): - data_blocks |= {*data_block} - - # Write the datablocks in a new blend file. - bpy.data.libraries.write(path, data_blocks) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': self.scene_type, - 'ext': self.scene_type, - 'files': filename, - "stagingDir": dir_path - } - instance.data["representations"].append(representation) - - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index f773a50ad2..c238cca633 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -127,8 +127,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "mvLook", "mvUsdComposition", "mvUsdOverride", - "simpleUnrealTexture", - "blenderScene" + "simpleUnrealTexture" ] exclude_families = ["render.farm"] db_representation_context_keys = [ From dae7ed439a47012010b0804cb6708f2660ac61e1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 16 Jun 2023 22:51:06 +0800 Subject: [PATCH 122/446] add the related families --- openpype/plugins/publish/collect_anatomy_frame_range.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_anatomy_frame_range.py index 71a5dcfeb0..134e7b6f72 100644 --- a/openpype/plugins/publish/collect_anatomy_frame_range.py +++ b/openpype/plugins/publish/collect_anatomy_frame_range.py @@ -9,6 +9,9 @@ class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.491 label = "Collect Anatomy Frame Range" + families = ["plate", "pointcache", + "vdbcache","online", + "render"] hosts = ["traypublisher"] def process(self, instance): From bbfe1566864d6ace9b29c97ab14b8586fcb89f5d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 16 Jun 2023 22:52:11 +0800 Subject: [PATCH 123/446] hound fix --- openpype/plugins/publish/collect_anatomy_frame_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_anatomy_frame_range.py index 134e7b6f72..05a3fa1a76 100644 --- a/openpype/plugins/publish/collect_anatomy_frame_range.py +++ b/openpype/plugins/publish/collect_anatomy_frame_range.py @@ -10,7 +10,7 @@ class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.491 label = "Collect Anatomy Frame Range" families = ["plate", "pointcache", - "vdbcache","online", + "vdbcache", "online", "render"] hosts = ["traypublisher"] From 30032214f6efd3ff34d90975b34a8c16f533f293 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 16 Jun 2023 15:52:38 +0100 Subject: [PATCH 124/446] Added support for camera --- .../hosts/blender/plugins/load/load_blend.py | 2 +- .../blender/plugins/load/load_camera_blend.py | 256 ------------------ 2 files changed, 1 insertion(+), 257 deletions(-) delete mode 100644 openpype/hosts/blender/plugins/load/load_camera_blend.py diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index b64793566f..53933a1934 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -20,7 +20,7 @@ from openpype.hosts.blender.api.pipeline import ( class BlendLoader(plugin.AssetLoader): """Load assets from a .blend file.""" - families = ["model", "rig", "layout"] + families = ["model", "rig", "layout", "camera"] representations = ["blend"] label = "Load Blend" diff --git a/openpype/hosts/blender/plugins/load/load_camera_blend.py b/openpype/hosts/blender/plugins/load/load_camera_blend.py deleted file mode 100644 index f00027f0b4..0000000000 --- a/openpype/hosts/blender/plugins/load/load_camera_blend.py +++ /dev/null @@ -1,256 +0,0 @@ -"""Load a camera asset in Blender.""" - -import logging -from pathlib import Path -from pprint import pformat -from typing import Dict, List, Optional - -import bpy - -from openpype.pipeline import ( - get_representation_path, - AVALON_CONTAINER_ID, -) -from openpype.hosts.blender.api import plugin -from openpype.hosts.blender.api.pipeline import ( - AVALON_CONTAINERS, - AVALON_PROPERTY, -) - -logger = logging.getLogger("openpype").getChild( - "blender").getChild("load_camera") - - -class BlendCameraLoader(plugin.AssetLoader): - """Load a camera from a .blend file. - - Warning: - Loading the same asset more then once is not properly supported at the - moment. - """ - - families = ["camera"] - representations = ["blend"] - - label = "Link Camera (Blend)" - icon = "code-fork" - color = "orange" - - def _remove(self, asset_group): - objects = list(asset_group.children) - - for obj in objects: - if obj.type == 'CAMERA': - bpy.data.cameras.remove(obj.data) - - def _process(self, libpath, asset_group, group_name): - with bpy.data.libraries.load( - libpath, link=True, relative=False - ) as (data_from, data_to): - data_to.objects = data_from.objects - - parent = bpy.context.scene.collection - - empties = [obj for obj in data_to.objects if obj.type == 'EMPTY'] - - container = None - - for empty in empties: - if empty.get(AVALON_PROPERTY): - container = empty - break - - assert container, "No asset group found" - - # Children must be linked before parents, - # otherwise the hierarchy will break - objects = [] - nodes = list(container.children) - - for obj in nodes: - obj.parent = asset_group - - for obj in nodes: - objects.append(obj) - nodes.extend(list(obj.children)) - - objects.reverse() - - for obj in objects: - parent.objects.link(obj) - - for obj in objects: - local_obj = plugin.prepare_data(obj, group_name) - - if local_obj.type != 'EMPTY': - plugin.prepare_data(local_obj.data, group_name) - - if not local_obj.get(AVALON_PROPERTY): - local_obj[AVALON_PROPERTY] = dict() - - avalon_info = local_obj[AVALON_PROPERTY] - avalon_info.update({"container_name": group_name}) - - objects.reverse() - - bpy.data.orphans_purge(do_local_ids=False) - - plugin.deselect_all() - - return objects - - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - - asset_name = plugin.asset_name(asset, subset) - unique_number = plugin.get_unique_number(asset, subset) - group_name = plugin.asset_name(asset, subset, unique_number) - namespace = namespace or f"{asset}_{unique_number}" - - avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) - if not avalon_container: - avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) - bpy.context.scene.collection.children.link(avalon_container) - - asset_group = bpy.data.objects.new(group_name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - avalon_container.objects.link(asset_group) - - objects = self._process(libpath, asset_group, group_name) - - bpy.context.scene.collection.objects.link(asset_group) - - asset_group[AVALON_PROPERTY] = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, - "name": name, - "namespace": namespace or '', - "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), - "libpath": libpath, - "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "family": context["representation"]["context"]["family"], - "objectName": group_name - } - - self[:] = objects - return objects - - def exec_update(self, container: Dict, representation: Dict): - """Update the loaded asset. - - This will remove all children of the asset group, load the new ones - and add them as children of the group. - """ - object_name = container["objectName"] - asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(representation)) - extension = libpath.suffix.lower() - - self.log.info( - "Container: %s\nRepresentation: %s", - pformat(container, indent=2), - pformat(representation, indent=2), - ) - - assert asset_group, ( - f"The asset is not loaded: {container['objectName']}" - ) - assert libpath, ( - "No existing library file found for {container['objectName']}" - ) - assert libpath.is_file(), ( - f"The file doesn't exist: {libpath}" - ) - assert extension in plugin.VALID_EXTENSIONS, ( - f"Unsupported file: {libpath}" - ) - - metadata = asset_group.get(AVALON_PROPERTY) - group_libpath = metadata["libpath"] - - normalized_group_libpath = ( - str(Path(bpy.path.abspath(group_libpath)).resolve()) - ) - normalized_libpath = ( - str(Path(bpy.path.abspath(str(libpath))).resolve()) - ) - self.log.debug( - "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", - normalized_group_libpath, - normalized_libpath, - ) - if normalized_group_libpath == normalized_libpath: - self.log.info("Library already loaded, not updating...") - return - - # Check how many assets use the same library - count = 0 - for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: - if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath: - count += 1 - - mat = asset_group.matrix_basis.copy() - - self._remove(asset_group) - - # If it is the last object to use that library, remove it - if count == 1: - library = bpy.data.libraries.get(bpy.path.basename(group_libpath)) - if library: - bpy.data.libraries.remove(library) - - self._process(str(libpath), asset_group, object_name) - - asset_group.matrix_basis = mat - - metadata["libpath"] = str(libpath) - metadata["representation"] = str(representation["_id"]) - metadata["parent"] = str(representation["parent"]) - - def exec_remove(self, container: Dict) -> bool: - """Remove an existing container from a Blender scene. - - Arguments: - container (openpype:container-1.0): Container to remove, - from `host.ls()`. - - Returns: - bool: Whether the container was deleted. - """ - object_name = container["objectName"] - asset_group = bpy.data.objects.get(object_name) - libpath = asset_group.get(AVALON_PROPERTY).get('libpath') - - # Check how many assets use the same library - count = 0 - for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: - if obj.get(AVALON_PROPERTY).get('libpath') == libpath: - count += 1 - - if not asset_group: - return False - - self._remove(asset_group) - - bpy.data.objects.remove(asset_group) - - # If it is the last object to use that library, remove it - if count == 1: - library = bpy.data.libraries.get(bpy.path.basename(libpath)) - bpy.data.libraries.remove(library) - - return True From 64d24f26bb6c99638eace695ec84677c790371eb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 18:21:01 +0800 Subject: [PATCH 125/446] roy and oscar's comments --- .../plugins/publish/collect_anatomy_frame_range.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_anatomy_frame_range.py index 05a3fa1a76..f59b381a5f 100644 --- a/openpype/plugins/publish/collect_anatomy_frame_range.py +++ b/openpype/plugins/publish/collect_anatomy_frame_range.py @@ -15,13 +15,15 @@ class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): hosts = ["traypublisher"] def process(self, instance): - self.log.info("Collecting Anatomy frame range.") + self.log.debug("Collecting Anatomy frame range.") asset_doc = instance.data.get("assetEntity") if not asset_doc: - self.log.info("Missing required data..") + self.log.debug("Instance has no asset entity set." + " Skipping collecting frame range data.") return asset_data = asset_doc["data"] + key_sets = [] for key in ( "fps", "frameStart", @@ -30,7 +32,8 @@ class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): "handleEnd" ): if key not in instance.data and key in asset_data: - value = asset_data[key] - instance.data[key] = value + instance.data[key] = asset_data[key] + key_sets.append(key) - self.log.info("Anatomy frame range collection finished.") + self.log.debug(f"Anatomy frame range data {key_sets} " + "has been collected from asset entity.") From dff197941e629ed21ed43e6b673c7aa0102e1381 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 19:39:04 +0800 Subject: [PATCH 126/446] roy's comment --- openpype/plugins/publish/collect_anatomy_frame_range.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_anatomy_frame_range.py index f59b381a5f..2da21d55e2 100644 --- a/openpype/plugins/publish/collect_anatomy_frame_range.py +++ b/openpype/plugins/publish/collect_anatomy_frame_range.py @@ -15,7 +15,6 @@ class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): hosts = ["traypublisher"] def process(self, instance): - self.log.debug("Collecting Anatomy frame range.") asset_doc = instance.data.get("assetEntity") if not asset_doc: self.log.debug("Instance has no asset entity set." From a57c6b58cc2d1c63e3d8b9a752e79001b266c028 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 20:11:25 +0800 Subject: [PATCH 127/446] roy's comment on docstring --- openpype/plugins/publish/collect_anatomy_frame_range.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_anatomy_frame_range.py index 2da21d55e2..381b927e63 100644 --- a/openpype/plugins/publish/collect_anatomy_frame_range.py +++ b/openpype/plugins/publish/collect_anatomy_frame_range.py @@ -2,9 +2,9 @@ import pyblish.api class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): - """Collect Frame Range specific Anatomy data. + """Collect Frame Range data. - Plugin is running for all instances on context even not active instances. + Plugin is running for all instances even not active ones. """ order = pyblish.api.CollectorOrder + 0.491 From a9181fe26e612616a9405517b0a600b300772880 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 20:43:26 +0800 Subject: [PATCH 128/446] rename the plugin --- ...ect_anatomy_frame_range.py => collect_frame_range_data.py} | 4 +--- .../settings/defaults/project_settings/traypublisher.json | 2 +- .../schemas/projects_schema/schema_project_traypublisher.json | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) rename openpype/plugins/publish/{collect_anatomy_frame_range.py => collect_frame_range_data.py} (91%) diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_frame_range_data.py similarity index 91% rename from openpype/plugins/publish/collect_anatomy_frame_range.py rename to openpype/plugins/publish/collect_frame_range_data.py index 381b927e63..9110439645 100644 --- a/openpype/plugins/publish/collect_anatomy_frame_range.py +++ b/openpype/plugins/publish/collect_frame_range_data.py @@ -2,9 +2,7 @@ import pyblish.api class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): - """Collect Frame Range data. - - Plugin is running for all instances even not active ones. + """Collect Frame Range data. """ order = pyblish.api.CollectorOrder + 0.491 diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 1f1c7115be..b7a01e584b 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -329,7 +329,7 @@ } }, "publish": { - "CollectAnatomyFrameRange": { + "CollectFrameRangeData": { "enabled": true, "optional": true, "active": true diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 53b060d3ca..9e3a1b703e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -350,8 +350,8 @@ "name": "template_validate_plugin", "template_data": [ { - "key": "CollectAnatomyFrameRange", - "label": "Collect Anatomy frame range" + "key": "CollectFrameRangeData", + "label": "Collect frame range data" }, { "key": "ValidateFrameRange", From 87ddaa1756861977ed3f3796026719c83b7edde9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 20:51:23 +0800 Subject: [PATCH 129/446] refactor the collector --- openpype/plugins/publish/collect_frame_range_data.py | 4 ++-- .../schemas/projects_schema/schema_project_traypublisher.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/collect_frame_range_data.py b/openpype/plugins/publish/collect_frame_range_data.py index 9110439645..ff773d4c59 100644 --- a/openpype/plugins/publish/collect_frame_range_data.py +++ b/openpype/plugins/publish/collect_frame_range_data.py @@ -1,12 +1,12 @@ import pyblish.api -class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): +class CollectFrameRangeData(pyblish.api.InstancePlugin): """Collect Frame Range data. """ order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Anatomy Frame Range" + label = "Collect Frame Range from Asset Entity" families = ["plate", "pointcache", "vdbcache", "online", "render"] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 9e3a1b703e..4a54c6b4b4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -351,7 +351,7 @@ "template_data": [ { "key": "CollectFrameRangeData", - "label": "Collect frame range data" + "label": "Collect frame range from asset entity" }, { "key": "ValidateFrameRange", From 191876cd1502acf872d10e5f7711c569c47fd665 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 20 Jun 2023 22:04:24 +0800 Subject: [PATCH 130/446] ondrej's comment --- openpype/plugins/load/delivery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 79f1fd745d..3afedbd5a2 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -91,7 +91,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): renumber_frame = QtWidgets.QCheckBox() renumber_frame_value = QtWidgets.QSpinBox() - renumber_frame_value.setMinimum(-(1 << 32) // 2) + max_int = (1 << 32) // 2 + renumber_frame_value.setRange(-max_int, max_int - 1) root_line_edit = QtWidgets.QLineEdit() From 10d0fda1a27343b9546a8e4ac73d78bc59c2ec2b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 20 Jun 2023 16:18:29 +0200 Subject: [PATCH 131/446] :recycle: switch bgeo to poincache and differentiate abc workflow alembic and bgeo are now both under pointcache families, their individual plugins differentiated --- openpype/hosts/houdini/plugins/create/create_bgeo.py | 7 +++++-- .../hosts/houdini/plugins/create/create_pointcache.py | 4 ++++ openpype/hosts/houdini/plugins/publish/extract_alembic.py | 2 +- openpype/hosts/houdini/plugins/publish/extract_bgeo.py | 8 +++++++- .../plugins/publish/validate_abc_primitive_to_detail.py | 2 +- .../houdini/plugins/publish/validate_alembic_face_sets.py | 2 +- .../plugins/publish/validate_alembic_input_node.py | 2 +- .../houdini/plugins/publish/validate_file_extension.py | 3 +-- .../plugins/publish/validate_primitive_hierarchy_paths.py | 2 +- 9 files changed, 22 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index 468215d76d..6edc2e76d2 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -9,7 +9,7 @@ class CreateBGEO(plugin.HoudiniCreator): """BGEO pointcache creator.""" identifier = "io.openpype.creators.houdini.bgeo" label = "BGEO PointCache" - family = "bgeo" + family = "pointcache" icon = "gears" def create(self, subset_name, instance_data, pre_create_data): @@ -18,7 +18,10 @@ class CreateBGEO(plugin.HoudiniCreator): instance_data.pop("active", None) instance_data.update({"node_type": "geometry"}) - instance_data["bgeo_type"] = pre_create_data.get("bgeo_type") + + if not instance_data.get("families"): + instance_data["families"] = [] + instance_data["families"] += ["bgeo"] instance = super(CreateBGEO, self).create( subset_name, diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index df74070fee..2e8ced19c6 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -19,6 +19,10 @@ class CreatePointCache(plugin.HoudiniCreator): instance_data.pop("active", None) instance_data.update({"node_type": "alembic"}) + if not instance_data.get("families"): + instance_data["families"] = [] + instance_data["families"] += ["abc"] + instance = super(CreatePointCache, self).create( subset_name, instance_data, diff --git a/openpype/hosts/houdini/plugins/publish/extract_alembic.py b/openpype/hosts/houdini/plugins/publish/extract_alembic.py index cb2d4ef424..bdd19b23d4 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_alembic.py +++ b/openpype/hosts/houdini/plugins/publish/extract_alembic.py @@ -13,7 +13,7 @@ class ExtractAlembic(publish.Extractor): order = pyblish.api.ExtractorOrder label = "Extract Alembic" hosts = ["houdini"] - families = ["pointcache", "camera"] + families = ["abc", "camera"] def process(self, instance): diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py index 23c3b78813..4d876cb181 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -4,6 +4,7 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop +from openpype.hosts.houdini.api import lib import hou @@ -35,12 +36,17 @@ class ExtractBGEO(publish.Extractor): output = instance.data["frames"] + _, ext = lib.splitext( + output[0], allowed_multidot_extensions=[ + ".ass.gz", ".bgeo.sc", ".bgeo.gz", + ".bgeo.lzma", ".bgeo.bz2"]) + if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "bgeo", - "ext": instance.data["bgeo_type"], + "ext": ext.lstrip("."), "files": output, "stagingDir": staging_dir, "frameStart": instance.data["frameStart"], diff --git a/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py b/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py index 86e92a052f..94aa0edc96 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py +++ b/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py @@ -17,7 +17,7 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder + 0.1 - families = ["pointcache"] + families = ["abc"] hosts = ["houdini"] label = "Validate Primitive to Detail (Abc)" diff --git a/openpype/hosts/houdini/plugins/publish/validate_alembic_face_sets.py b/openpype/hosts/houdini/plugins/publish/validate_alembic_face_sets.py index 44d58cfa36..40114bc40e 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_alembic_face_sets.py +++ b/openpype/hosts/houdini/plugins/publish/validate_alembic_face_sets.py @@ -18,7 +18,7 @@ class ValidateAlembicROPFaceSets(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder + 0.1 - families = ["pointcache"] + families = ["abc"] hosts = ["houdini"] label = "Validate Alembic ROP Face Sets" diff --git a/openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py b/openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py index b0cf4cdc58..47c47e4ea2 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py @@ -14,7 +14,7 @@ class ValidateAlembicInputNode(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder + 0.1 - families = ["pointcache"] + families = ["abc"] hosts = ["houdini"] label = "Validate Input Node (Abc)" diff --git a/openpype/hosts/houdini/plugins/publish/validate_file_extension.py b/openpype/hosts/houdini/plugins/publish/validate_file_extension.py index 4584e78f4f..6594d10851 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_file_extension.py +++ b/openpype/hosts/houdini/plugins/publish/validate_file_extension.py @@ -19,12 +19,11 @@ class ValidateFileExtension(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["pointcache", "camera", "vdbcache"] + families = ["camera", "vdbcache"] hosts = ["houdini"] label = "Output File Extension" family_extensions = { - "pointcache": ".abc", "camera": ".abc", "vdbcache": ".vdb", } diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index d3a4c0cfbf..599e1797b2 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -15,7 +15,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): """ order = ValidateContentsOrder + 0.1 - families = ["pointcache"] + families = ["abc"] hosts = ["houdini"] label = "Validate Prims Hierarchy Path" From 1bce2eae7012c2153e93aad4da56b8211e75cfa7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 21 Jun 2023 15:59:40 +0800 Subject: [PATCH 132/446] get the frameStart and frameEnd from the representation data --- .../publish/collect_frame_range_data.py | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/collect_frame_range_data.py b/openpype/plugins/publish/collect_frame_range_data.py index ff773d4c59..211870fa9d 100644 --- a/openpype/plugins/publish/collect_frame_range_data.py +++ b/openpype/plugins/publish/collect_frame_range_data.py @@ -1,4 +1,5 @@ import pyblish.api +import clique class CollectFrameRangeData(pyblish.api.InstancePlugin): @@ -11,15 +12,50 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin): "vdbcache", "online", "render"] hosts = ["traypublisher"] + img_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", + "gif", "svg"] + video_extensions = ["avi", "mov", "mp4"] def process(self, instance): + repres = instance.data.get("representations") + asset_data = None asset_doc = instance.data.get("assetEntity") if not asset_doc: self.log.debug("Instance has no asset entity set." - " Skipping collecting frame range data.") + " Skipping collecting frame range data.") return + if repres: + first_repre = repres[0] + ext = first_repre["ext"].replace(".", "") + if not ext or ext.lower() not in self.img_extensions: + self.log.warning(f"Cannot find file extension " + " in representation data") + return + if ext in self.video_extensions: + self.log.info("Collecting frame range data" + " not supported for video extensions") + return + + files = first_repre["files"] + repres_file = clique.assemble( + files, minimum_items=1)[0][0] + repres_frames = [frames for frames in repres_file.indexes] + last_frame = len(repres_frames) - 1 + entity_data = asset_doc["data"] + asset_data = { + "fps": entity_data["fps"], + "frameStart": repres_frames[0], + "frameEnd": repres_frames[last_frame], + "handleStart": entity_data["handleStart"], + "handleEnd": entity_data["handleEnd"] + } + + else: + self.log.info("No representation data.. " + "\nUse Asset Entity data instead") + + asset_data = asset_doc["data"] - asset_data = asset_doc["data"] key_sets = [] for key in ( "fps", @@ -32,5 +68,5 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin): instance.data[key] = asset_data[key] key_sets.append(key) - self.log.debug(f"Anatomy frame range data {key_sets} " + self.log.debug(f"Frame range data {key_sets} " "has been collected from asset entity.") From 7971a9901d584450c91ac01c3d9b90224f59e523 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 21 Jun 2023 16:01:00 +0800 Subject: [PATCH 133/446] hound fix --- openpype/plugins/publish/collect_frame_range_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_frame_range_data.py b/openpype/plugins/publish/collect_frame_range_data.py index 211870fa9d..65198ba314 100644 --- a/openpype/plugins/publish/collect_frame_range_data.py +++ b/openpype/plugins/publish/collect_frame_range_data.py @@ -22,7 +22,7 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin): asset_doc = instance.data.get("assetEntity") if not asset_doc: self.log.debug("Instance has no asset entity set." - " Skipping collecting frame range data.") + " Skipping collecting frame range data.") return if repres: first_repre = repres[0] From d6e5687a26f3b97ef166dfcaf77856e362866d1a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 21 Jun 2023 17:45:22 +0800 Subject: [PATCH 134/446] roy's comment --- .../publish/collect_arnold_scene_source.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index f160a3a0c5..0edc0f9205 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -21,15 +21,11 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): if members is None: self.log.warning("Skipped empty instance: \"%s\" " % objset) continue - if objset.endswith("content_SET"): - members = cmds.ls(members, long=True) - children = get_all_children(members) - instance.data["contentMembers"] = children - self.log.debug("content members: {}".format(children)) - elif objset.endswith("proxy_SET"): - set_members = get_all_children(cmds.ls(members, long=True)) - instance.data["proxy"] = set_members - self.log.debug("proxy members: {}".format(set_members)) + + instance.data["contentMembers"] = self.get_ass_data( + objset, members, "content_SET") + instance.data["proxy"] = self.get_ass_data( + objset, members, "proxy_SET") # Use camera in object set if present else default to render globals # camera. @@ -48,3 +44,13 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): self.log.debug("No renderable cameras found.") self.log.debug("data: {}".format(instance.data)) + + def get_ass_data(self, objset, members, suffix): + if objset.endswith(suffix): + members = cmds.ls(members, long=True) + if not members: + return + children = get_all_children(members) + members = list(set(members) - set(children)) + + return children + members From 4b371b6c59ea94b935bf245370bcc32608ad2a50 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 21 Jun 2023 18:45:28 +0800 Subject: [PATCH 135/446] revert the unnecessary code --- .../publish/collect_arnold_scene_source.py | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index 0edc0f9205..f160a3a0c5 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -21,11 +21,15 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): if members is None: self.log.warning("Skipped empty instance: \"%s\" " % objset) continue - - instance.data["contentMembers"] = self.get_ass_data( - objset, members, "content_SET") - instance.data["proxy"] = self.get_ass_data( - objset, members, "proxy_SET") + if objset.endswith("content_SET"): + members = cmds.ls(members, long=True) + children = get_all_children(members) + instance.data["contentMembers"] = children + self.log.debug("content members: {}".format(children)) + elif objset.endswith("proxy_SET"): + set_members = get_all_children(cmds.ls(members, long=True)) + instance.data["proxy"] = set_members + self.log.debug("proxy members: {}".format(set_members)) # Use camera in object set if present else default to render globals # camera. @@ -44,13 +48,3 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): self.log.debug("No renderable cameras found.") self.log.debug("data: {}".format(instance.data)) - - def get_ass_data(self, objset, members, suffix): - if objset.endswith(suffix): - members = cmds.ls(members, long=True) - if not members: - return - children = get_all_children(members) - members = list(set(members) - set(children)) - - return children + members From dae6257f881ca5044cdc6d708b1bb2eb6dede547 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 21 Jun 2023 16:48:11 +0200 Subject: [PATCH 136/446] :bug: handle legacy pointcache instance conversion --- openpype/hosts/houdini/plugins/create/convert_legacy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/convert_legacy.py b/openpype/hosts/houdini/plugins/create/convert_legacy.py index e549c9dc26..86103e3369 100644 --- a/openpype/hosts/houdini/plugins/create/convert_legacy.py +++ b/openpype/hosts/houdini/plugins/create/convert_legacy.py @@ -69,6 +69,8 @@ class HoudiniLegacyConvertor(SubsetConvertorPlugin): "creator_identifier": self.family_to_id[family], "instance_node": subset.path() } + if family == "pointcache": + data["families"] = ["abc"] self.log.info("Converting {} to {}".format( subset.path(), self.family_to_id[family])) imprint(subset, data) From 2725c87065767a47e9836f453d667643278e285e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 21 Jun 2023 16:49:01 +0200 Subject: [PATCH 137/446] :heavy_minus_sign: remove bgeo family from integration --- openpype/plugins/publish/integrate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 54b5e56868..f392cf67f7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -137,8 +137,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "mvUsdOverride", "simpleUnrealTexture", "online", - "uasset", - "bgeo" + "uasset" ] default_template_name = "publish" From 35c4d89742a853c0cc4e0c5d6c8d10a448fbd928 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 21 Jun 2023 17:07:35 +0200 Subject: [PATCH 138/446] :speech_balloon: enhance validation error message --- .../houdini/plugins/publish/validate_bgeo_file_sop_path.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py b/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py index f4016a1b09..1736e58cdc 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py @@ -18,8 +18,9 @@ class ValidateNoSOPPath(pyblish.api.InstancePlugin): node = hou.node(instance.data.get("instance_node")) sop_path = node.evalParm("soppath") if not sop_path: - raise PublishValidationError("Empty SOP Path found in " - "the bgeo isntance Geometry") + raise PublishValidationError( + ("Empty SOP Path ('soppath' parameter) found in " + f"the BGEO instance Geometry - {node.path()}")) if not isinstance(hou.node(sop_path), hou.SopNode): raise PublishValidationError( "SOP path is not pointing to valid SOP node.") From ec5355bfdeeda55bc7a90ba206c463f5d47c56bb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 21 Jun 2023 17:09:11 +0200 Subject: [PATCH 139/446] :rotating_light: fix hound issue --- .../houdini/plugins/publish/validate_bgeo_file_sop_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py b/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py index 1736e58cdc..22746aabb0 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py @@ -19,7 +19,7 @@ class ValidateNoSOPPath(pyblish.api.InstancePlugin): sop_path = node.evalParm("soppath") if not sop_path: raise PublishValidationError( - ("Empty SOP Path ('soppath' parameter) found in " + ("Empty SOP Path ('soppath' parameter) found in " f"the BGEO instance Geometry - {node.path()}")) if not isinstance(hou.node(sop_path), hou.SopNode): raise PublishValidationError( From 6ddf12e79a48f30a96e2002142b942dcdcb24e80 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 26 Jun 2023 18:05:40 +0800 Subject: [PATCH 140/446] fabia's comment & add just frame range data from representation --- .../plugins/publish/collect_frame_range_data.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/collect_frame_range_data.py b/openpype/plugins/publish/collect_frame_range_data.py index 65198ba314..90dfd3e0a8 100644 --- a/openpype/plugins/publish/collect_frame_range_data.py +++ b/openpype/plugins/publish/collect_frame_range_data.py @@ -7,7 +7,7 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin): """ order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Frame Range from Asset Entity" + label = "Collect Frame Range Data" families = ["plate", "pointcache", "vdbcache", "online", "render"] @@ -19,11 +19,6 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin): def process(self, instance): repres = instance.data.get("representations") asset_data = None - asset_doc = instance.data.get("assetEntity") - if not asset_doc: - self.log.debug("Instance has no asset entity set." - " Skipping collecting frame range data.") - return if repres: first_repre = repres[0] ext = first_repre["ext"].replace(".", "") @@ -41,19 +36,19 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin): files, minimum_items=1)[0][0] repres_frames = [frames for frames in repres_file.indexes] last_frame = len(repres_frames) - 1 - entity_data = asset_doc["data"] asset_data = { - "fps": entity_data["fps"], "frameStart": repres_frames[0], "frameEnd": repres_frames[last_frame], - "handleStart": entity_data["handleStart"], - "handleEnd": entity_data["handleEnd"] } else: self.log.info("No representation data.. " "\nUse Asset Entity data instead") - + asset_doc = instance.data.get("assetEntity") + if instance.data.get("frameStart") is not None or not asset_doc: + self.log.debug("Instance has no asset entity set." + " Skipping collecting frame range data.") + return asset_data = asset_doc["data"] key_sets = [] From ce37b597f2a24bb71660bd8a345ab6c1edf198b4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 26 Jun 2023 18:06:22 +0800 Subject: [PATCH 141/446] hound fix --- openpype/plugins/publish/collect_frame_range_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_frame_range_data.py b/openpype/plugins/publish/collect_frame_range_data.py index 90dfd3e0a8..9129a7f0f5 100644 --- a/openpype/plugins/publish/collect_frame_range_data.py +++ b/openpype/plugins/publish/collect_frame_range_data.py @@ -45,7 +45,7 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin): self.log.info("No representation data.. " "\nUse Asset Entity data instead") asset_doc = instance.data.get("assetEntity") - if instance.data.get("frameStart") is not None or not asset_doc: + if instance.data.get("frameStart") is not None or not asset_doc: self.log.debug("Instance has no asset entity set." " Skipping collecting frame range data.") return From 6619be9bc72159aa0cfe60d63403bfa28b903a5a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 27 Jun 2023 11:49:29 +0200 Subject: [PATCH 142/446] Fix append job --- .../plugins/publish/create_maya_royalrender_job.py | 5 ++--- .../plugins/publish/create_nuke_royalrender_job.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 5ad3c90bdc..695f33cc35 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- """Submitting render job to RoyalRender.""" -# -*- coding: utf-8 -*- -"""Submitting render job to RoyalRender.""" import os from maya.OpenMaya import MGlobal @@ -12,6 +10,7 @@ from openpype.pipeline.farm.tools import iter_expected_files class CreateMayaRoyalRenderJob(lib.BaseCreateRoyalRenderJob): label = "Create Maya Render job in RR" + hosts = ["maya"] families = ["renderlayer"] def update_job_with_host_specific(self, instance, job): @@ -39,4 +38,4 @@ class CreateMayaRoyalRenderJob(lib.BaseCreateRoyalRenderJob): layer_name) job = self.update_job_with_host_specific(instance, job) - instance.data["rrJobs"] += job + instance.data["rrJobs"].append(job) diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py index 62636a6744..7b4a66a920 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py @@ -26,7 +26,7 @@ class CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob): for job in jobs: job = self.update_job_with_host_specific(instance, job) - instance.data["rrJobs"] += jobs + instance.data["rrJobs"].append(job) def update_job_with_host_specific(self, instance, job): nuke_version = re.search( From d6b0b9c74c70d33f57374229a1c3352237da53a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 27 Jun 2023 11:50:25 +0200 Subject: [PATCH 143/446] Remove outputDir --- openpype/modules/royalrender/lib.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/modules/royalrender/lib.py b/openpype/modules/royalrender/lib.py index ce78b1a738..1433bac6a2 100644 --- a/openpype/modules/royalrender/lib.py +++ b/openpype/modules/royalrender/lib.py @@ -131,9 +131,6 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, if not self._instance.data.get("rrJobs"): self._instance.data["rrJobs"] = [] - self._instance.data["outputDir"] = os.path.dirname( - self._instance.data["path"]).replace("\\", "/") - def get_job(self, instance, script_path, render_path, node_name): """Get RR job based on current instance. From d0e1d5c36fbaa96d12bb0b372aa207e7ec000be4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 27 Jun 2023 18:29:33 +0200 Subject: [PATCH 144/446] Fix cleanup of camera info Without it .mel script in RR would fail with syntax error. --- .../royalrender/plugins/publish/create_maya_royalrender_job.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 695f33cc35..69e54f54b7 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -16,7 +16,8 @@ class CreateMayaRoyalRenderJob(lib.BaseCreateRoyalRenderJob): def update_job_with_host_specific(self, instance, job): job.Software = "Maya" job.Version = "{0:.2f}".format(MGlobal.apiVersion() / 10000) - job.Camera = instance.data["cameras"][0], + if instance.data.get("cameras"): + job.Camera = instance.data["cameras"][0].replace("'", '"') workspace = instance.context.data["workspaceDir"] job.SceneDatabaseDir = workspace From ed3f2649d327928a1eef6356175359c9b8566089 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Jun 2023 17:50:39 +0800 Subject: [PATCH 145/446] Jakub's comment --- openpype/pipeline/delivery.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index abb666189d..54ec935a83 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -121,8 +121,8 @@ def deliver_single_file( format_dict, report_items, log, - renumber_frame=False, - re_frame_value=0 + has_renumber_shot=False, + shot_number=0 ): """Copy single file to calculated path based on template @@ -164,7 +164,7 @@ def deliver_single_file( if not os.path.exists(delivery_folder): os.makedirs(delivery_folder) - if renumber_frame: + if has_renumber_shot and shot_number != 0: src_dir = os.path.dirname(src_path) src_filepaths = os.listdir(src_dir) src_collections, remainders = clique.assemble(src_filepaths) @@ -173,7 +173,7 @@ def deliver_single_file( src_indexes = list(src_collection.indexes) destination_indexes = [ - re_frame_value + idx + shot_number + idx for idx in range(len(src_indexes)) ] @@ -212,8 +212,8 @@ def deliver_sequence( format_dict, report_items, log, - renumber_frame=False, - re_frame_value=0 + has_renumber_shot=False, + shot_number=0 ): """ For Pype2(mainly - works in 3 too) where representation might not contain files. @@ -336,8 +336,8 @@ def deliver_sequence( os.path.join(dir_path, src_file_name) ) - if renumber_frame: - dsp_index = (re_frame_value - int(index)) + 1 + if has_renumber_shot and shot_number != 0: + dsp_index = (shot_number - int(index)) + 1 if dsp_index < 0: msg = "Renumber frame has a smaller number than original frame" # noqa report_items[msg].append(src_file_name) From 6d7220e94f8b3ea4fe7b85de4ecc46ac2cf88589 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Jun 2023 17:53:43 +0800 Subject: [PATCH 146/446] Jakub's comment --- openpype/pipeline/delivery.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 54ec935a83..3483251b29 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -164,7 +164,7 @@ def deliver_single_file( if not os.path.exists(delivery_folder): os.makedirs(delivery_folder) - if has_renumber_shot and shot_number != 0: + if has_renumber_shot: src_dir = os.path.dirname(src_path) src_filepaths = os.listdir(src_dir) src_collections, remainders = clique.assemble(src_filepaths) @@ -336,15 +336,15 @@ def deliver_sequence( os.path.join(dir_path, src_file_name) ) - if has_renumber_shot and shot_number != 0: - dsp_index = (shot_number - int(index)) + 1 - if dsp_index < 0: + if has_renumber_shot: + dst_index = (shot_number - int(index)) + 1 + if dst_index < 0: msg = "Renumber frame has a smaller number than original frame" # noqa report_items[msg].append(src_file_name) log.warning("{} <{}>".format(msg, context)) return report_items, 0 - dst_padding = dst_collection.format("{padding}") % dsp_index + dst_padding = dst_collection.format("{padding}") % dst_index dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) dst = os.path.normpath( os.path.join(delivery_folder, dst) From d92337adeb10f826bd22a743039ef1cf13fd6779 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Jun 2023 17:58:12 +0800 Subject: [PATCH 147/446] Jakub's comment --- openpype/pipeline/delivery.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 3483251b29..88084c5aad 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -337,7 +337,11 @@ def deliver_sequence( ) if has_renumber_shot: - dst_index = (shot_number - int(index)) + 1 + dst_index = None + if shot_number != 0: + dst_index = (shot_number - int(index)) + 1 + else: + dst_index = shot_number - int(index) if dst_index < 0: msg = "Renumber frame has a smaller number than original frame" # noqa report_items[msg].append(src_file_name) From f757edd644e68ca70489700ebf77692714b0f276 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Jun 2023 18:50:22 +0800 Subject: [PATCH 148/446] implement more concise variable --- openpype/pipeline/delivery.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 88084c5aad..6ac81055f7 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -121,8 +121,8 @@ def deliver_single_file( format_dict, report_items, log, - has_renumber_shot=False, - shot_number=0 + has_renumbered_frame=False, + renumber_frame=0 ): """Copy single file to calculated path based on template @@ -164,7 +164,7 @@ def deliver_single_file( if not os.path.exists(delivery_folder): os.makedirs(delivery_folder) - if has_renumber_shot: + if has_renumbered_frame: src_dir = os.path.dirname(src_path) src_filepaths = os.listdir(src_dir) src_collections, remainders = clique.assemble(src_filepaths) @@ -173,7 +173,7 @@ def deliver_single_file( src_indexes = list(src_collection.indexes) destination_indexes = [ - shot_number + idx + renumber_frame + idx for idx in range(len(src_indexes)) ] @@ -212,8 +212,8 @@ def deliver_sequence( format_dict, report_items, log, - has_renumber_shot=False, - shot_number=0 + has_renumbered_frame=False, + renumber_frame=0 ): """ For Pype2(mainly - works in 3 too) where representation might not contain files. @@ -336,12 +336,12 @@ def deliver_sequence( os.path.join(dir_path, src_file_name) ) - if has_renumber_shot: + if has_renumbered_frame: dst_index = None - if shot_number != 0: - dst_index = (shot_number - int(index)) + 1 + if renumber_frame != 0: + dst_index = (renumber_frame - int(index)) + 1 else: - dst_index = shot_number - int(index) + dst_index = renumber_frame - int(index) if dst_index < 0: msg = "Renumber frame has a smaller number than original frame" # noqa report_items[msg].append(src_file_name) From ec54515b8caf08773ef721b6a9e7a6edc3e4ecb5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 28 Jun 2023 12:53:28 +0200 Subject: [PATCH 149/446] Fix - correct resolution of published workile path --- openpype/modules/royalrender/lib.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/modules/royalrender/lib.py b/openpype/modules/royalrender/lib.py index 1433bac6a2..cbddb22b4e 100644 --- a/openpype/modules/royalrender/lib.py +++ b/openpype/modules/royalrender/lib.py @@ -113,12 +113,15 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, self.scene_path = context.data["currentFile"] if self.use_published: - file_path = get_published_workfile_instance(context) + published_workfile = get_published_workfile_instance(context) # fallback if nothing was set - if not file_path: + if published_workfile is None: self.log.warning("Falling back to workfile") file_path = context.data["currentFile"] + else: + workfile_repre = published_workfile.data["representations"][0] + file_path = workfile_repre["published_path"] self.scene_path = file_path self.log.info( From 0df104be54146921944fa32a02f98fac4db455d1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 28 Jun 2023 15:10:57 +0200 Subject: [PATCH 150/446] Fix - proper padding of file name RR requires replacing frame value with # properly padded. --- openpype/modules/royalrender/lib.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/modules/royalrender/lib.py b/openpype/modules/royalrender/lib.py index cbddb22b4e..006ebc5bfd 100644 --- a/openpype/modules/royalrender/lib.py +++ b/openpype/modules/royalrender/lib.py @@ -155,7 +155,7 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, batch_name += datetime.now().strftime("%d%m%Y%H%M%S") render_dir = os.path.normpath(os.path.dirname(render_path)) - output_filename_0 = self.preview_fname(render_path) + output_filename_0 = self.pad_file_name(render_path, str(start_frame)) file_name, file_ext = os.path.splitext( os.path.basename(output_filename_0)) @@ -278,14 +278,16 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, ) return expected_files - def preview_fname(self, path): + def pad_file_name(self, path, first_frame): """Return output file path with #### for padding. RR requires the path to be formatted with # in place of numbers. For example `/path/to/render.####.png` Args: - path (str): path to rendered images + path (str): path to rendered image + first_frame (str): from representation to cleany replace with # + padding Returns: str @@ -298,4 +300,10 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, return int(search_results[1]) if "#" in path: self.log.debug("_ path: `{}`".format(path)) + return path + + if first_frame: + padding = len(first_frame) + path = path.replace(first_frame, "#" * padding) + return path From 4a672c5c12ee8d8b3659f08d1a581f31f272ca20 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 28 Jun 2023 15:15:36 +0200 Subject: [PATCH 151/446] Hound --- openpype/modules/deadline/abstract_submit_deadline.py | 3 +-- .../royalrender/plugins/publish/create_nuke_royalrender_job.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index b63a43598d..6ff9afbc42 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -23,8 +23,7 @@ from openpype.pipeline.publish import ( OpenPypePyblishPluginMixin ) from openpype.pipeline.publish.lib import ( - replace_published_scene, - get_published_workfile_instance + replace_published_scene ) JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py index 7b4a66a920..763792bdc2 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py @@ -41,8 +41,6 @@ class CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob): """Nuke creates multiple RR jobs - for baking etc.""" # get output path render_path = instance.data['path'] - self.log.info("render::{}".format(render_path)) - self.log.info("expected::{}".format(instance.data.get("expectedFiles"))) script_path = self.scene_path node = self._instance.data["transientData"]["node"] From 44acfd3c9f953098687910dd37fe4d04fb27b19d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 29 Jun 2023 17:34:45 +0800 Subject: [PATCH 152/446] use files from representation for delivery action --- openpype/pipeline/delivery.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 6ac81055f7..c86c1c8e28 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -166,7 +166,16 @@ def deliver_single_file( if has_renumbered_frame: src_dir = os.path.dirname(src_path) - src_filepaths = os.listdir(src_dir) + src_filepaths =[] + for repre_file in repre["files"]: + src_path = anatomy.fill_root(repre_file["path"]) + base_path = os.path.basename(src_path) + src_filepaths.append(base_path) + if not src_filepaths: + msg = "Source files not found, cannot find collection" + report_items[msg].append(src_path) + log.warning("{} <{}>".format(msg, src_filepaths)) + return report_items, 0 src_collections, remainders = clique.assemble(src_filepaths) src_collection = src_collections[0] From a34e8c163f74a041ccf13a4ae868888a9e1edc06 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 29 Jun 2023 17:35:24 +0800 Subject: [PATCH 153/446] hound fix --- openpype/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index c86c1c8e28..fefc80d3c5 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -166,7 +166,7 @@ def deliver_single_file( if has_renumbered_frame: src_dir = os.path.dirname(src_path) - src_filepaths =[] + src_filepaths = [] for repre_file in repre["files"]: src_path = anatomy.fill_root(repre_file["path"]) base_path = os.path.basename(src_path) From 20fd5bbbbe05ae9e28dd9da498fdefd0feac4f76 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 29 Jun 2023 21:06:19 +0800 Subject: [PATCH 154/446] add function for renumbering the frame in either absolute or relative way --- openpype/pipeline/delivery.py | 44 +++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index fefc80d3c5..4a5d2084f2 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -176,18 +176,13 @@ def deliver_single_file( report_items[msg].append(src_path) log.warning("{} <{}>".format(msg, src_filepaths)) return report_items, 0 - src_collections, remainders = clique.assemble(src_filepaths) + src_collections, remainders = clique.assemble(src_filepaths) src_collection = src_collections[0] src_indexes = list(src_collection.indexes) - destination_indexes = [ - renumber_frame + idx - for idx in range(len(src_indexes)) - ] - dst_filepaths = [] - for index in destination_indexes: + for index in src_indexes: template_data = copy.deepcopy(anatomy_data) template_data["frame"] = index template_obj = anatomy.templates_obj["delivery"][template_name] @@ -197,6 +192,9 @@ def deliver_single_file( dst_filepaths.append(template_filled) dst_collection = clique.assemble(dst_filepaths)[0][0] + relative = not has_renumbered_frame + dst_collection = shift_collection( + dst_collection, renumber_frame, relative=relative) for src_file_name, dst in zip(src_collection, dst_collection): src_path = os.path.join(src_dir, src_file_name) @@ -374,3 +372,35 @@ def deliver_sequence( uploaded += 1 return report_items, uploaded + + +def shift_collection(collection, start_offset, relative=True): + """Shift frames of a clique.Collection. + + When relative is True `start_offset` will be an offset from + the current start frame of the collection shifting it by `start_offset`. + When relative is False `start_offset` will be the new start + frame of the sequence - shifting the rest of the frames along. + + Arguments: + collection (clique.Collection): Input collection. + start_offset (int): Offset to apply (or start frame to set if + relative is False) + relative (bool): Whether the start offset is relative or the + the absolute new start frame. + + Returns: + clique.Collection: Shifted collection + + """ + first_frame = next(iter(collection.indexes)) + if relative: + shift = start_offset + else: + shift = start_offset - first_frame + return clique.Collection( + head=collection.head, + tail=collection.tail, + padding=collection.padding, + indexes={idx + shift for idx in collection.indexes} + ) From 32873bdc22ce69e01de9ec15c629c5e8af43cfe5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Jul 2023 19:30:08 +0800 Subject: [PATCH 155/446] add functions for filtering in dailog in custom attr maxscript --- openpype/hosts/max/api/plugin.py | 43 ++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 14b0653f40..cb3a3b9e8d 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -15,6 +15,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" parameters main rollout:OPparams ( all_handles type:#maxObjectTab tabSize:0 tabSizeVariable:on + sel_list type:#stringTab tabSize:0 tabSizeVariable:on ) rollout OPparams "OP Parameters" @@ -30,11 +31,41 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" handle_name = obj_name + "<" + handle as string + ">" return handle_name ) + fn nodes_to_add node = + ( + sceneObjs = #() + n = node as string + for obj in Objects do + ( + tmp_obj = obj as string + append sceneObjs tmp_obj + ) + if sel_list != undefined do + ( + for obj in sel_list do + ( + idx = findItem sceneObjs obj + if idx do + ( + deleteItem sceneObjs idx + ) + ) + ) + idx = findItem sceneObjs n + if idx then return true else false + ) + + fn nodes_to_rmv node = + ( + n = node as string + idx = findItem sel_list n + if idx then return true else false + ) on button_add pressed do ( current_selection = selectByName title:"Select Objects to add to - the Container" buttontext:"Add" + the Container" buttontext:"Add" filter:nodes_to_add if current_selection == undefined then return False temp_arr = #() i_node_arr = #() @@ -46,8 +77,10 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" if idx do ( continue ) + name = c as string append temp_arr handle_name append i_node_arr node_ref + append sel_list name ) all_handles = join i_node_arr all_handles list_node.items = join temp_arr list_node.items @@ -56,7 +89,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" on button_del pressed do ( current_selection = selectByName title:"Select Objects to remove - from the Container" buttontext:"Remove" + from the Container" buttontext:"Remove" filter: nodes_to_rmv if current_selection == undefined then return False temp_arr = #() i_node_arr = #() @@ -67,6 +100,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( node_ref = NodeTransformMonitor node:c as string handle_name = node_to_name c + n = c as string tmp_all_handles = #() for i in all_handles do ( @@ -84,6 +118,11 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( new_temp_arr = DeleteItem list_node.items idx ) + idx = finditem sel_list n + if idx do + ( + sel_list = DeleteItem sel_list idx + ) ) all_handles = join i_node_arr new_i_node_arr list_node.items = join temp_arr new_temp_arr From b9e362d1e8a0edb4c7c2bc44d5af8be8a94a6b37 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 Jul 2023 17:15:59 +0200 Subject: [PATCH 156/446] Fix - Nuke needs to have multiple jobs First job is pure rendering, then there could be baking etc. scripts. --- .../royalrender/plugins/publish/create_nuke_royalrender_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py index 763792bdc2..7f503c80a9 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py @@ -26,7 +26,7 @@ class CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob): for job in jobs: job = self.update_job_with_host_specific(instance, job) - instance.data["rrJobs"].append(job) + instance.data["rrJobs"].append(job) def update_job_with_host_specific(self, instance, job): nuke_version = re.search( From 27e18d96e3abf506cafd9c3719a4eac51b1333bf Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 09:13:41 +0300 Subject: [PATCH 157/446] solution as an action --- .../validate_primitive_hierarchy_paths.py | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index d3a4c0cfbf..888a4cae25 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -1,10 +1,18 @@ # -*- coding: utf-8 -*- import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import ( + ValidateContentsOrder, + RepairAction, +) + import hou +class AddDefaultPathAction(RepairAction): + label = "Add a default path attribute" + icon = "mdi.pencil-plus-outline" + class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): """Validate all primitives build hierarchy from attribute when enabled. @@ -18,6 +26,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): families = ["pointcache"] hosts = ["houdini"] label = "Validate Prims Hierarchy Path" + actions = [AddDefaultPathAction] def process(self, instance): invalid = self.get_invalid(instance) @@ -104,3 +113,46 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): "(%s of %s prims)" % (path_attr, len(invalid_prims), num_prims) ) return [output_node.path()] + + @classmethod + def repair(cls, instance): + output_node = instance.data.get("output_node") + rop_node = hou.node(instance.data["instance_node"]) + + path_attr = rop_node.parm("path_attrib").eval() + + frame = instance.data.get("frameStart", 0) + geo = output_node.geometryAtFrame(frame) + + paths = geo.findPrimAttrib(path_attr) and \ + geo.primStringAttribValues(path_attr) + + if paths: + return + + path_node = output_node.parent().createNode("name", "AUTO_PATH") + path_node.parm("attribname").set(path_attr) + path_node.parm("name1").set('`opname("..")`/`opname("..")`Shape') + + cls.log.debug( + '%s node has been created' + % path_node + ) + + path_node.setGenericFlag(hou.nodeFlag.DisplayComment,True) + path_node.setComment( + 'Auto path node created automatically by "Add a default path attribute"' + '\nFeel free to modify or replace it.' + ) + + if output_node.type().name() in ['null', 'output']: + # Connect before + path_node.setFirstInput(output_node.input(0)) + path_node.moveToGoodPosition() + output_node.setFirstInput(path_node) + output_node.moveToGoodPosition() + else: + # Connect after + output_node.setFirstInput(path_node) + rop_node.parm('sop_path').set(path_node.path()) + path_node.moveToGoodPosition() From dae7f64c673f921212816028d5034cf37df50bc5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 09:37:14 +0300 Subject: [PATCH 158/446] add doc string --- .../plugins/publish/validate_primitive_hierarchy_paths.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index 888a4cae25..d680f5fb24 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -116,6 +116,12 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): + """Add a default path attribute Action. + + it's a helper action more than a repair action, + which used to add a default single value. + """ + output_node = instance.data.get("output_node") rop_node = hou.node(instance.data["instance_node"]) From 0a055a9b867ee2b0ce2c9896e043411285b0726f Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 09:37:59 +0300 Subject: [PATCH 159/446] better debugging message --- .../plugins/publish/validate_primitive_hierarchy_paths.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index d680f5fb24..a77aa89fef 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -141,8 +141,8 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): path_node.parm("name1").set('`opname("..")`/`opname("..")`Shape') cls.log.debug( - '%s node has been created' - % path_node + "'%s' was created. It adds '%s' with a default single value" + % (path_node, path_attr) ) path_node.setGenericFlag(hou.nodeFlag.DisplayComment,True) From 853de24a9edb56a9f99ce955ed008bc10cf0c9fe Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 09:50:39 +0300 Subject: [PATCH 160/446] fix bug when connecting after --- .../publish/validate_primitive_hierarchy_paths.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index a77aa89fef..8159af4895 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -159,6 +159,13 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): output_node.moveToGoodPosition() else: # Connect after - output_node.setFirstInput(path_node) + path_node.setFirstInput(output_node) rop_node.parm('sop_path').set(path_node.path()) path_node.moveToGoodPosition() + instance.data.update({"output_node" : path_node }) + + cls.log.debug( + "'%s' has set as the output node, " + "'%s' has updated" + % (path_node, rop_node) + ) From e302bffe5308d265515fdc6a1ac3ce2e5ed553b8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 09:52:36 +0300 Subject: [PATCH 161/446] add a comment --- .../plugins/publish/validate_primitive_hierarchy_paths.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index 8159af4895..e70c5cf3fd 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -133,6 +133,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): paths = geo.findPrimAttrib(path_attr) and \ geo.primStringAttribValues(path_attr) + # This check to prevent the action from running multiple times. if paths: return From 3b6a59f5c87cf34c025f64c0ce5ba68bb6f1312e Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 18:18:01 +0300 Subject: [PATCH 162/446] update action --- .../validate_primitive_hierarchy_paths.py | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index e70c5cf3fd..4bc7c56f85 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -13,6 +13,7 @@ class AddDefaultPathAction(RepairAction): label = "Add a default path attribute" icon = "mdi.pencil-plus-outline" + class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): """Validate all primitives build hierarchy from attribute when enabled. @@ -48,7 +49,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): "Ensure a valid SOP output path is set." % rop_node.path() ) - return [rop_node.path()] + return [rop_node] build_from_path = rop_node.parm("build_from_path").eval() if not build_from_path: @@ -65,7 +66,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): "value set, but 'Build Hierarchy from Attribute'" "is enabled." ) - return [rop_node.path()] + return [rop_node] cls.log.debug("Checking for attribute: %s" % path_attr) @@ -92,7 +93,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): "Geometry Primitives are missing " "path attribute: `%s`" % path_attr ) - return [output_node.path()] + return [output_node] # Ensure at least a single string value is present if not attrib.strings(): @@ -100,7 +101,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): "Primitive path attribute has no " "string values: %s" % path_attr ) - return [output_node.path()] + return [output_node] paths = geo.primStringAttribValues(path_attr) # Ensure all primitives are set to a valid path @@ -112,7 +113,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): "Prims have no value for attribute `%s` " "(%s of %s prims)" % (path_attr, len(invalid_prims), num_prims) ) - return [output_node.path()] + return [output_node] @classmethod def repair(cls, instance): @@ -122,21 +123,24 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): which used to add a default single value. """ - output_node = instance.data.get("output_node") rop_node = hou.node(instance.data["instance_node"]) + output_node = rop_node.parm('sop_path').evalAsNode() - path_attr = rop_node.parm("path_attrib").eval() - - frame = instance.data.get("frameStart", 0) - geo = output_node.geometryAtFrame(frame) - - paths = geo.findPrimAttrib(path_attr) and \ - geo.primStringAttribValues(path_attr) + if not output_node: + cls.log.debug( + "Action isn't performed, empty SOP Path on %s" + % rop_node + ) + return # This check to prevent the action from running multiple times. - if paths: + # git_invalid only returns [output_node] when + # path attribute is the problem + if cls.get_invalid(instance) != [output_node]: return + path_attr = rop_node.parm("path_attrib").eval() + path_node = output_node.parent().createNode("name", "AUTO_PATH") path_node.parm("attribname").set(path_attr) path_node.parm("name1").set('`opname("..")`/`opname("..")`Shape') @@ -163,10 +167,8 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): path_node.setFirstInput(output_node) rop_node.parm('sop_path').set(path_node.path()) path_node.moveToGoodPosition() - instance.data.update({"output_node" : path_node }) cls.log.debug( - "'%s' has set as the output node, " - "'%s' has updated" - % (path_node, rop_node) + "SOP path on '%s' updated to new output node '%s'" + % (rop_node, path_node) ) From 614d600564643fdd4ebcc543a2571425611ea385 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 4 Jul 2023 17:21:09 +0200 Subject: [PATCH 163/446] Refactor - removed unnecessary variable Fixed expectedFiles, they need to extended (at least in Nuke, first renders, then baking etc.) --- openpype/modules/royalrender/lib.py | 21 +++++++----------- .../publish/create_maya_royalrender_job.py | 6 ++--- .../publish/create_nuke_royalrender_job.py | 22 +++++++++---------- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/openpype/modules/royalrender/lib.py b/openpype/modules/royalrender/lib.py index 006ebc5bfd..4708d25eed 100644 --- a/openpype/modules/royalrender/lib.py +++ b/openpype/modules/royalrender/lib.py @@ -79,7 +79,6 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, ] def __init__(self, *args, **kwargs): - self._instance = None self._rr_root = None self.scene_path = None self.job = None @@ -99,7 +98,6 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, "suspend_publish"] context = instance.context - self._instance = instance self._rr_root = self._resolve_rr_path(context, instance.data.get( "rrPathName")) # noqa @@ -128,11 +126,11 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, "Using published scene for render {}".format(self.scene_path) ) - if not self._instance.data.get("expectedFiles"): - self._instance.data["expectedFiles"] = [] + if not instance.data.get("expectedFiles"): + instance.data["expectedFiles"] = [] - if not self._instance.data.get("rrJobs"): - self._instance.data["rrJobs"] = [] + if not instance.data.get("rrJobs"): + instance.data["rrJobs"] = [] def get_job(self, instance, script_path, render_path, node_name): """Get RR job based on current instance. @@ -150,7 +148,7 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, end_frame = int(instance.data["frameEndHandle"]) batch_name = os.path.basename(script_path) - jobname = "%s - %s" % (batch_name, self._instance.name) + jobname = "%s - %s" % (batch_name, instance.name) if is_in_tests(): batch_name += datetime.now().strftime("%d%m%Y%H%M%S") @@ -252,9 +250,6 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, list: List of expected files. """ - if instance.data.get("expectedFiles"): - return instance.data["expectedFiles"] - dir_name = os.path.dirname(path) file = os.path.basename(path) @@ -269,7 +264,7 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, expected_files.append(path) return expected_files - if self._instance.data.get("slate"): + if instance.data.get("slate"): start_frame -= 1 expected_files.extend( @@ -293,13 +288,13 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, str """ - self.log.debug("_ path: `{}`".format(path)) + self.log.debug("pad_file_name path: `{}`".format(path)) if "%" in path: search_results = re.search(r"(%0)(\d)(d.)", path).groups() self.log.debug("_ search_results: `{}`".format(search_results)) return int(search_results[1]) if "#" in path: - self.log.debug("_ path: `{}`".format(path)) + self.log.debug("already padded: `{}`".format(path)) return path if first_frame: diff --git a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py index 69e54f54b7..22d910b7cd 100644 --- a/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_maya_royalrender_job.py @@ -27,12 +27,12 @@ class CreateMayaRoyalRenderJob(lib.BaseCreateRoyalRenderJob): """Plugin entry point.""" super(CreateMayaRoyalRenderJob, self).process(instance) - expected_files = self._instance.data["expectedFiles"] + expected_files = instance.data["expectedFiles"] first_file_path = next(iter_expected_files(expected_files)) output_dir = os.path.dirname(first_file_path) - self._instance.data["outputDir"] = output_dir + instance.data["outputDir"] = output_dir - layer = self._instance.data["setMembers"] # type: str + layer = instance.data["setMembers"] # type: str layer_name = layer.removeprefix("rs_") job = self.get_job(instance, self.scene_path, first_file_path, diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py index 7f503c80a9..6db0fe959e 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py @@ -15,22 +15,22 @@ class CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob): super(CreateNukeRoyalRenderJob, self).process(instance) # redefinition of families - if "render" in self._instance.data["family"]: - self._instance.data["family"] = "write" - self._instance.data["families"].insert(0, "render2d") - elif "prerender" in self._instance.data["family"]: - self._instance.data["family"] = "write" - self._instance.data["families"].insert(0, "prerender") + if "render" in instance.data["family"]: + instance.data["family"] = "write" + instance.data["families"].insert(0, "render2d") + elif "prerender" in instance.data["family"]: + instance.data["family"] = "write" + instance.data["families"].insert(0, "prerender") - jobs = self.create_jobs(self._instance) + jobs = self.create_jobs(instance) for job in jobs: job = self.update_job_with_host_specific(instance, job) - instance.data["rrJobs"].append(job) + instance.data["rrJobs"].append(job) def update_job_with_host_specific(self, instance, job): nuke_version = re.search( - r"\d+\.\d+", self._instance.context.data.get("hostVersion")) + r"\d+\.\d+", instance.context.data.get("hostVersion")) job.Software = "Nuke" job.Version = nuke_version.group() @@ -42,7 +42,7 @@ class CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob): # get output path render_path = instance.data['path'] script_path = self.scene_path - node = self._instance.data["transientData"]["node"] + node = instance.data["transientData"]["node"] # main job jobs = [ @@ -54,7 +54,7 @@ class CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob): ) ] - for baking_script in self._instance.data.get("bakingNukeScripts", []): + for baking_script in instance.data.get("bakingNukeScripts", []): render_path = baking_script["bakeRenderPath"] script_path = baking_script["bakeScriptPath"] exe_node_name = baking_script["bakeWriteNodeName"] From 8754345b44ebb7747a96bcf6386b702596cf9429 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 4 Jul 2023 17:35:42 +0200 Subject: [PATCH 164/446] Fix - Nuke needs to have multiple jobs got accidentally reverted It needs to loop through jobs to add them all. --- .../royalrender/plugins/publish/create_nuke_royalrender_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py index 6db0fe959e..71daa6edf8 100644 --- a/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_nuke_royalrender_job.py @@ -26,7 +26,7 @@ class CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob): for job in jobs: job = self.update_job_with_host_specific(instance, job) - instance.data["rrJobs"].append(job) + instance.data["rrJobs"].append(job) def update_job_with_host_specific(self, instance, job): nuke_version = re.search( From 6e2fa0299ebdaece4ee64ee49dd55327f306bcc2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 5 Jul 2023 09:42:47 +0300 Subject: [PATCH 165/446] fix linting --- .../plugins/publish/validate_primitive_hierarchy_paths.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index 4bc7c56f85..a5a4469b9f 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -136,7 +136,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): # This check to prevent the action from running multiple times. # git_invalid only returns [output_node] when # path attribute is the problem - if cls.get_invalid(instance) != [output_node]: + if cls.get_invalid(instance) != [output_node]: return path_attr = rop_node.parm("path_attrib").eval() @@ -169,6 +169,6 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): path_node.moveToGoodPosition() cls.log.debug( - "SOP path on '%s' updated to new output node '%s'" - % (rop_node, path_node) + "SOP path on '%s' updated to new output node '%s'" + % (rop_node, path_node) ) From 7c21bf61bbec2902f3c2208372d6dd82d8d86351 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 17:12:31 +0800 Subject: [PATCH 166/446] Jakub's comment --- openpype/pipeline/delivery.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 4a5d2084f2..a7d8e1dcb6 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -122,7 +122,7 @@ def deliver_single_file( report_items, log, has_renumbered_frame=False, - renumber_frame=0 + new_frame_start=0 ): """Copy single file to calculated path based on template @@ -194,7 +194,7 @@ def deliver_single_file( dst_collection = clique.assemble(dst_filepaths)[0][0] relative = not has_renumbered_frame dst_collection = shift_collection( - dst_collection, renumber_frame, relative=relative) + dst_collection, new_frame_start, relative=relative) for src_file_name, dst in zip(src_collection, dst_collection): src_path = os.path.join(src_dir, src_file_name) @@ -220,7 +220,7 @@ def deliver_sequence( report_items, log, has_renumbered_frame=False, - renumber_frame=0 + new_frame_start=0 ): """ For Pype2(mainly - works in 3 too) where representation might not contain files. @@ -345,10 +345,10 @@ def deliver_sequence( if has_renumbered_frame: dst_index = None - if renumber_frame != 0: - dst_index = (renumber_frame - int(index)) + 1 + if new_frame_start != 0: + dst_index = (new_frame_start - int(index)) + 1 else: - dst_index = renumber_frame - int(index) + dst_index = new_frame_start - int(index) if dst_index < 0: msg = "Renumber frame has a smaller number than original frame" # noqa report_items[msg].append(src_file_name) From 4cc4bccbb55fda3cf148bbba7cc2a0c58163fee6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 18:28:58 +0800 Subject: [PATCH 167/446] Jakub's comment --- openpype/pipeline/delivery.py | 106 ++++-------------------------- openpype/plugins/load/delivery.py | 4 +- 2 files changed, 16 insertions(+), 94 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index a7d8e1dcb6..fa4b8aee83 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -164,47 +164,8 @@ def deliver_single_file( if not os.path.exists(delivery_folder): os.makedirs(delivery_folder) - if has_renumbered_frame: - src_dir = os.path.dirname(src_path) - src_filepaths = [] - for repre_file in repre["files"]: - src_path = anatomy.fill_root(repre_file["path"]) - base_path = os.path.basename(src_path) - src_filepaths.append(base_path) - if not src_filepaths: - msg = "Source files not found, cannot find collection" - report_items[msg].append(src_path) - log.warning("{} <{}>".format(msg, src_filepaths)) - return report_items, 0 - - src_collections, remainders = clique.assemble(src_filepaths) - src_collection = src_collections[0] - src_indexes = list(src_collection.indexes) - - dst_filepaths = [] - for index in src_indexes: - template_data = copy.deepcopy(anatomy_data) - template_data["frame"] = index - template_obj = anatomy.templates_obj["delivery"][template_name] - template_filled = template_obj.format_strict( - template_data - ) - dst_filepaths.append(template_filled) - - dst_collection = clique.assemble(dst_filepaths)[0][0] - relative = not has_renumbered_frame - dst_collection = shift_collection( - dst_collection, new_frame_start, relative=relative) - - for src_file_name, dst in zip(src_collection, dst_collection): - src_path = os.path.join(src_dir, src_file_name) - delivery_path = os.path.join(delivery_folder, dst) - log.debug("Copying single: {} -> {}".format( - src_path, delivery_path)) - _copy_file(src_path, delivery_path) - else: - log.debug("Copying single: {} -> {}".format(src_path, delivery_path)) - _copy_file(src_path, delivery_path) + log.debug("Copying single: {} -> {}".format(src_path, delivery_path)) + _copy_file(src_path, delivery_path) return report_items, 1 @@ -336,71 +297,30 @@ def deliver_sequence( src_head = src_collection.head src_tail = src_collection.tail uploaded = 0 + first_frame = next(iter(src_collection.indexes)) for index in src_collection.indexes: src_padding = src_collection.format("{padding}") % index src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) src = os.path.normpath( os.path.join(dir_path, src_file_name) ) - + dst_index = index if has_renumbered_frame: - dst_index = None - if new_frame_start != 0: - dst_index = (new_frame_start - int(index)) + 1 - else: - dst_index = new_frame_start - int(index) + # Calculate offset between first frame and current frame + # - '0' for first frame + offset = new_frame_start - first_frame + # Add offset to new frame start + dst_index = index + offset if dst_index < 0: msg = "Renumber frame has a smaller number than original frame" # noqa report_items[msg].append(src_file_name) log.warning("{} <{}>".format(msg, context)) return report_items, 0 - - dst_padding = dst_collection.format("{padding}") % dst_index - dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) - dst = os.path.normpath( - os.path.join(delivery_folder, dst) - ) - - _copy_file(src, dst) - - else: - dst_padding = dst_collection.format("{padding}") % index - dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) - log.debug("Copying single: {} -> {}".format(src, dst)) - _copy_file(src, dst) + dst_padding = dst_collection.format("{padding}") % dst_index + dst = "{}{}{}".format(dst_head, dst_padding, dst_tail) + log.debug("Copying single: {} -> {}".format(src, dst)) + _copy_file(src, dst) uploaded += 1 return report_items, uploaded - - -def shift_collection(collection, start_offset, relative=True): - """Shift frames of a clique.Collection. - - When relative is True `start_offset` will be an offset from - the current start frame of the collection shifting it by `start_offset`. - When relative is False `start_offset` will be the new start - frame of the sequence - shifting the rest of the frames along. - - Arguments: - collection (clique.Collection): Input collection. - start_offset (int): Offset to apply (or start frame to set if - relative is False) - relative (bool): Whether the start offset is relative or the - the absolute new start frame. - - Returns: - clique.Collection: Shifted collection - - """ - first_frame = next(iter(collection.indexes)) - if relative: - shift = start_offset - else: - shift = start_offset - first_frame - return clique.Collection( - head=collection.head, - tail=collection.tail, - padding=collection.padding, - indexes={idx + shift for idx in collection.indexes} - ) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 3afedbd5a2..e49d62aa6d 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -225,7 +225,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): args[0] = src_path if frame: anatomy_data["frame"] = frame - new_report_items, uploaded = deliver_single_file(*args) + new_report_items, uploaded = deliver_sequence(*args) + else: + new_report_items, uploaded = deliver_single_file(*args) report_items.update(new_report_items) self._update_progress(uploaded) else: # fallback for Pype2 and representations without files From 106b59018a4c0fac3b9557d788ece4b0e966fc03 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 18:32:32 +0800 Subject: [PATCH 168/446] remove unused args --- openpype/pipeline/delivery.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index fa4b8aee83..9a44ec1b84 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -120,9 +120,7 @@ def deliver_single_file( anatomy_data, format_dict, report_items, - log, - has_renumbered_frame=False, - new_frame_start=0 + log ): """Copy single file to calculated path based on template From 4a3c9ce4b665fda0081950f3be742afb01fc3218 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 19:18:03 +0800 Subject: [PATCH 169/446] filter the container --- openpype/hosts/max/api/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index cb3a3b9e8d..01ffc3ab27 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -34,6 +34,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" fn nodes_to_add node = ( sceneObjs = #() + if classOf node == Container do return false n = node as string for obj in Objects do ( @@ -53,6 +54,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ) idx = findItem sceneObjs n if idx then return true else false + if classOf node == Container do return false ) fn nodes_to_rmv node = From dab513f01c7258b9858782cae3f9213c0734e3b1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 20:03:53 +0800 Subject: [PATCH 170/446] jakub's comment --- openpype/pipeline/delivery.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 9a44ec1b84..1806ffdd6b 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -165,7 +165,6 @@ def deliver_single_file( log.debug("Copying single: {} -> {}".format(src_path, delivery_path)) _copy_file(src_path, delivery_path) - return report_items, 1 From be10dc7349924edbe6ae795d01a9d058a7e1e4bf Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 21:06:31 +0800 Subject: [PATCH 171/446] Jakub's comment --- openpype/pipeline/delivery.py | 4 +++- openpype/plugins/load/delivery.py | 32 ++++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 1806ffdd6b..e0c3ced626 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -120,7 +120,9 @@ def deliver_single_file( anatomy_data, format_dict, report_items, - log + log, + has_renumbered_frame=False, + new_frame_start=0 ): """Copy single file to calculated path based on template diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 1d38b6f0a0..f56f776dc6 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -237,13 +237,35 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): src_paths.append(src_path) sources_and_frames = collect_frames(src_paths) + frames = set(sources_and_frames.values()) + frames.discard(None) + first_frame = None + if frames: + first_frame = next(iter(frames)) + + for src_path, frame in sources_and_frames.items(): args[0] = src_path - if frame: - anatomy_data["frame"] = frame - new_report_items, uploaded = deliver_sequence(*args) - else: - new_report_items, uploaded = deliver_single_file(*args) + # Renumber frames + if renumber_frame and frame is not None: + # Calculate offset between first frame and current frame + # - '0' for first frame + offset = frame_offset - int(first_frame) + # Add offset to new frame start + frame = int(frame) + dst_frame = frame + offset + if dst_frame < 0: + msg = "Renumber frame has a smaller number than original frame" # noqa + report_items[msg].append(src_path) + self.log.warning("{} <{}>".format( + msg, dst_frame)) + continue + frame = dst_frame + + if frame is not None: + if frame: + anatomy_data["frame"] = frame + new_report_items, uploaded = deliver_single_file(*args) report_items.update(new_report_items) self._update_progress(uploaded) else: # fallback for Pype2 and representations without files From d7256a09925de1963c13b8799a1b6200554b4a04 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 21:07:45 +0800 Subject: [PATCH 172/446] hound fix --- openpype/plugins/load/delivery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index f56f776dc6..3d4b90686e 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -248,7 +248,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): args[0] = src_path # Renumber frames if renumber_frame and frame is not None: - # Calculate offset between first frame and current frame + # Calculate offset between + # first frame and current frame # - '0' for first frame offset = frame_offset - int(first_frame) # Add offset to new frame start From 708819f8b13980ebacea6468f15e6b4e2cf9051c Mon Sep 17 00:00:00 2001 From: Pype Club Date: Wed, 5 Jul 2023 16:50:58 +0200 Subject: [PATCH 173/446] Refactor - renamed replace_published_scene Former name pointed to replacing of whole file. This function is only about using path to published workfile instead of work workfile. --- openpype/modules/deadline/abstract_submit_deadline.py | 5 +++-- openpype/pipeline/publish/lib.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 6ff9afbc42..d9d250fe9e 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -23,7 +23,7 @@ from openpype.pipeline.publish import ( OpenPypePyblishPluginMixin ) from openpype.pipeline.publish.lib import ( - replace_published_scene + replace_with_published_scene_path ) JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) @@ -528,7 +528,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, published. """ - return replace_published_scene(self._instance, replace_in_path=True) + return replace_with_published_scene_path( + self._instance, replace_in_path=replace_in_path) def assemble_payload( self, job_info=None, plugin_info=None, aux_files=None): diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index a03303bc39..5d2a88fa3b 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -882,10 +882,11 @@ def get_published_workfile_instance(context): return i -def replace_published_scene(instance, replace_in_path=True): - """Switch work scene for published scene. +def replace_with_published_scene_path(instance, replace_in_path=True): + """Switch work scene path for published scene. If rendering/exporting from published scenes is enabled, this will replace paths from working scene to published scene. + This only works if publish contains workfile instance! Args: instance (pyblish.api.Instance): Pyblish instance. replace_in_path (bool): if True, it will try to find From 26cd4f0029473aa361bfe0b46139a1e0b0f30109 Mon Sep 17 00:00:00 2001 From: Pype Club Date: Wed, 5 Jul 2023 16:51:51 +0200 Subject: [PATCH 174/446] Hound --- openpype/pipeline/farm/pyblish_functions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 0ace02edb9..afd2ef3eeb 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -822,8 +822,6 @@ def attach_instances_to_subset(attach_to, instances): list: List of attached instances. """ - # - new_instances = [] for attach_instance in attach_to: for i in instances: From ef812bff3dc7134eb1242516a23df70fc36f6d7e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 22:55:30 +0800 Subject: [PATCH 175/446] roy's comment --- openpype/pipeline/delivery.py | 4 +- .../publish/collect_frame_range_data.py | 58 ++++++++++++------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 500f54040a..53f3d45e14 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -157,6 +157,8 @@ def deliver_single_file( delivery_path = delivery_path.replace("..", ".") # Make sure path is valid for all platforms delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) + # Remove newlines from the end of the string to avoid OSError during copy + delivery_path = delivery_path.rstrip() delivery_folder = os.path.dirname(delivery_path) if not os.path.exists(delivery_folder): @@ -305,4 +307,4 @@ def deliver_sequence( _copy_file(src, dst) uploaded += 1 - return report_items, uploaded + return report_items, uploaded \ No newline at end of file diff --git a/openpype/plugins/publish/collect_frame_range_data.py b/openpype/plugins/publish/collect_frame_range_data.py index 9129a7f0f5..ceea530676 100644 --- a/openpype/plugins/publish/collect_frame_range_data.py +++ b/openpype/plugins/publish/collect_frame_range_data.py @@ -1,8 +1,12 @@ import pyblish.api import clique +from openpype.pipeline import publish +from openpype.lib import BoolDef +from openpype.lib.transcoding import IMAGE_EXTENSIONS -class CollectFrameRangeData(pyblish.api.InstancePlugin): +class CollectFrameRangeData(pyblish.api.InstancePlugin, + publish.OpenPypePyblishPluginMixin): """Collect Frame Range data. """ @@ -12,44 +16,50 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin): "vdbcache", "online", "render"] hosts = ["traypublisher"] - img_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", - "gif", "svg"] - video_extensions = ["avi", "mov", "mp4"] def process(self, instance): repres = instance.data.get("representations") asset_data = None if repres: first_repre = repres[0] - ext = first_repre["ext"].replace(".", "") - if not ext or ext.lower() not in self.img_extensions: - self.log.warning(f"Cannot find file extension " + ext = ".{}".format(first_repre["ext"]) + if "ext" not in first_repre: + self.log.warning(f"Cannot find file extension" " in representation data") return - if ext in self.video_extensions: + if ext not in IMAGE_EXTENSIONS: self.log.info("Collecting frame range data" - " not supported for video extensions") + " only supported for image extensions") return files = first_repre["files"] - repres_file = clique.assemble( - files, minimum_items=1)[0][0] - repres_frames = [frames for frames in repres_file.indexes] - last_frame = len(repres_frames) - 1 + repres_files, remainder = clique.assemble(files) + repres_frames = list() + for repres_file in repres_files: + repres_frames = list(repres_file.indexes) asset_data = { "frameStart": repres_frames[0], - "frameEnd": repres_frames[last_frame], + "frameEnd": repres_frames[-1], } else: - self.log.info("No representation data.. " - "\nUse Asset Entity data instead") + self.log.info( + "No representation data.. Use Asset Entity data instead") asset_doc = instance.data.get("assetEntity") - if instance.data.get("frameStart") is not None or not asset_doc: - self.log.debug("Instance has no asset entity set." - " Skipping collecting frame range data.") + + attr_values = self.get_attr_values_from_data(instance.data) + if attr_values.get("setAssetFrameRange", True): + if instance.data.get("frameStart") is not None or not asset_doc: + self.log.debug("Instance has no asset entity set." + " Skipping collecting frame range data.") + return + self.log.debug( + "Falling back to collect frame range" + " data from asset entity set.") + asset_data = asset_doc["data"] + else: + self.log.debug("Skipping collecting frame range data.") return - asset_data = asset_doc["data"] key_sets = [] for key in ( @@ -65,3 +75,11 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin): self.log.debug(f"Frame range data {key_sets} " "has been collected from asset entity.") + + @classmethod + def get_attribute_defs(cls): + return [ + BoolDef("setAssetFrameRange", + label="Set Asset Frame Range", + default=False), + ] From fed1d096e81a84f8e32656f3af7f0ce98ce6b433 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 22:59:19 +0800 Subject: [PATCH 176/446] hound fix --- openpype/plugins/publish/collect_frame_range_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_frame_range_data.py b/openpype/plugins/publish/collect_frame_range_data.py index ceea530676..efa240e503 100644 --- a/openpype/plugins/publish/collect_frame_range_data.py +++ b/openpype/plugins/publish/collect_frame_range_data.py @@ -49,9 +49,9 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin, attr_values = self.get_attr_values_from_data(instance.data) if attr_values.get("setAssetFrameRange", True): - if instance.data.get("frameStart") is not None or not asset_doc: + if instance.data.get("frameStart") is not None or not asset_doc: # noqa self.log.debug("Instance has no asset entity set." - " Skipping collecting frame range data.") + " Skipping collecting frame range data.") return self.log.debug( "Falling back to collect frame range" From 32eb96c2c6e103f58a577ce10ef6ae373245aec6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Jul 2023 23:01:48 +0800 Subject: [PATCH 177/446] restore the unrelated code --- openpype/pipeline/delivery.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 53f3d45e14..500f54040a 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -157,8 +157,6 @@ def deliver_single_file( delivery_path = delivery_path.replace("..", ".") # Make sure path is valid for all platforms delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) - # Remove newlines from the end of the string to avoid OSError during copy - delivery_path = delivery_path.rstrip() delivery_folder = os.path.dirname(delivery_path) if not os.path.exists(delivery_folder): @@ -307,4 +305,4 @@ def deliver_sequence( _copy_file(src, dst) uploaded += 1 - return report_items, uploaded \ No newline at end of file + return report_items, uploaded From 5b735c70f5400ad034b2bf9bcf686175ef3881af Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Jul 2023 12:48:06 +0800 Subject: [PATCH 178/446] roy's comment --- .../publish/collect_frame_range_data.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/collect_frame_range_data.py b/openpype/plugins/publish/collect_frame_range_data.py index efa240e503..c25aacd7f3 100644 --- a/openpype/plugins/publish/collect_frame_range_data.py +++ b/openpype/plugins/publish/collect_frame_range_data.py @@ -2,7 +2,6 @@ import pyblish.api import clique from openpype.pipeline import publish from openpype.lib import BoolDef -from openpype.lib.transcoding import IMAGE_EXTENSIONS class CollectFrameRangeData(pyblish.api.InstancePlugin, @@ -22,21 +21,22 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin, asset_data = None if repres: first_repre = repres[0] - ext = ".{}".format(first_repre["ext"]) if "ext" not in first_repre: - self.log.warning(f"Cannot find file extension" + self.log.warning("Cannot find file extension" " in representation data") return - if ext not in IMAGE_EXTENSIONS: - self.log.info("Collecting frame range data" - " only supported for image extensions") - return files = first_repre["files"] - repres_files, remainder = clique.assemble(files) - repres_frames = list() - for repres_file in repres_files: - repres_frames = list(repres_file.indexes) + collections, remainder = clique.assemble(files) + if not collections: + # No sequences detected and we can't retrieve + # frame range + self.log.debug( + "No sequences detected in the representation data." + " Skipping collecting frame range data.") + return + collection = collections[0] + repres_frames = list(collection.indexes) asset_data = { "frameStart": repres_frames[0], "frameEnd": repres_frames[-1], @@ -44,7 +44,7 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin, else: self.log.info( - "No representation data.. Use Asset Entity data instead") + "No representation data. Using Asset Entity data instead") asset_doc = instance.data.get("assetEntity") attr_values = self.get_attr_values_from_data(instance.data) @@ -55,7 +55,7 @@ class CollectFrameRangeData(pyblish.api.InstancePlugin, return self.log.debug( "Falling back to collect frame range" - " data from asset entity set.") + " data from set asset entity.") asset_data = asset_doc["data"] else: self.log.debug("Skipping collecting frame range data.") From d78d1e0c25b8fe74121831b320ac5a20ff358811 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Jul 2023 16:31:56 +0800 Subject: [PATCH 179/446] remove redundency line --- openpype/hosts/max/api/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 01ffc3ab27..d8db716e6d 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -54,7 +54,6 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ) idx = findItem sceneObjs n if idx then return true else false - if classOf node == Container do return false ) fn nodes_to_rmv node = From 3f5d99e8335c47be24a9ed25652930c89a97fab7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 6 Jul 2023 12:09:57 +0300 Subject: [PATCH 180/446] update code style --- .../validate_primitive_hierarchy_paths.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index a5a4469b9f..7f6198294d 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -46,7 +46,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): if output_node is None: cls.log.error( "SOP Output node in '%s' does not exist. " - "Ensure a valid SOP output path is set." % rop_node.path() + "Ensure a valid SOP output path is set.", rop_node.path() ) return [rop_node] @@ -68,7 +68,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): ) return [rop_node] - cls.log.debug("Checking for attribute: %s" % path_attr) + cls.log.debug("Checking for attribute: %s", path_attr) # Check if the primitive attribute exists frame = instance.data.get("frameStart", 0) @@ -91,7 +91,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): if not attrib: cls.log.info( "Geometry Primitives are missing " - "path attribute: `%s`" % path_attr + "path attribute: `%s`", path_attr ) return [output_node] @@ -99,7 +99,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): if not attrib.strings(): cls.log.info( "Primitive path attribute has no " - "string values: %s" % path_attr + "string values: %s", path_attr ) return [output_node] @@ -111,7 +111,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): num_prims = len(geo.iterPrims()) # faster than len(geo.prims()) cls.log.info( "Prims have no value for attribute `%s` " - "(%s of %s prims)" % (path_attr, len(invalid_prims), num_prims) + "(%s of %s prims)", path_attr, len(invalid_prims), num_prims ) return [output_node] @@ -119,17 +119,17 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): def repair(cls, instance): """Add a default path attribute Action. - it's a helper action more than a repair action, - which used to add a default single value. + It is a helper action more than a repair action, + used to add a default single value for the path. """ rop_node = hou.node(instance.data["instance_node"]) - output_node = rop_node.parm('sop_path').evalAsNode() + output_node = rop_node.parm("sop_path").evalAsNode() if not output_node: cls.log.debug( - "Action isn't performed, empty SOP Path on %s" - % rop_node + "Action isn't performed, invalid SOP Path on %s", + rop_node ) return @@ -146,8 +146,8 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): path_node.parm("name1").set('`opname("..")`/`opname("..")`Shape') cls.log.debug( - "'%s' was created. It adds '%s' with a default single value" - % (path_node, path_attr) + "'%s' was created. It adds '%s' with a default single value", + path_node, path_attr ) path_node.setGenericFlag(hou.nodeFlag.DisplayComment,True) @@ -156,7 +156,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): '\nFeel free to modify or replace it.' ) - if output_node.type().name() in ['null', 'output']: + if output_node.type().name() in ["null", "output"]: # Connect before path_node.setFirstInput(output_node.input(0)) path_node.moveToGoodPosition() @@ -165,10 +165,10 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): else: # Connect after path_node.setFirstInput(output_node) - rop_node.parm('sop_path').set(path_node.path()) + rop_node.parm("sop_path").set(path_node.path()) path_node.moveToGoodPosition() cls.log.debug( - "SOP path on '%s' updated to new output node '%s'" - % (rop_node, path_node) + "SOP path on '%s' updated to new output node '%s'", + rop_node, path_node ) From 803236c9c59d5ce660bb1e8fcd6e883055fe49be Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Jul 2023 17:52:58 +0800 Subject: [PATCH 181/446] adding frame collector and optional frame data from asset entity collector --- .../collect_frame_range_asset_entity.py | 39 +++++++++ .../publish/collect_frame_range_data.py | 85 ------------------- .../publish/collect_sequence_frame_data.py | 48 +++++++++++ .../project_settings/traypublisher.json | 2 +- .../schema_project_traypublisher.json | 2 +- 5 files changed, 89 insertions(+), 87 deletions(-) create mode 100644 openpype/plugins/publish/collect_frame_range_asset_entity.py delete mode 100644 openpype/plugins/publish/collect_frame_range_data.py create mode 100644 openpype/plugins/publish/collect_sequence_frame_data.py diff --git a/openpype/plugins/publish/collect_frame_range_asset_entity.py b/openpype/plugins/publish/collect_frame_range_asset_entity.py new file mode 100644 index 0000000000..2f7570466c --- /dev/null +++ b/openpype/plugins/publish/collect_frame_range_asset_entity.py @@ -0,0 +1,39 @@ +import pyblish.api +from openpype.pipeline import OptionalPyblishPluginMixin + + +class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Collect Frame Range data From Asset Entity + """ + + order = pyblish.api.CollectorOrder + 0.3 + label = "Collect Frame Data From Asset Entity" + families = ["plate", "pointcache", + "vdbcache", "online", + "render"] + hosts = ["traypublisher"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + missing_keys = [] + for key in ( + "fps", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd" + ): + if key not in instance.data: + missing_keys.append(key) + key_sets = [] + for key in missing_keys: + asset_data = instance.data["assetEntity"]["data"] + if key in asset_data: + instance.data[key] = asset_data[key] + key_sets.append(key) + if key_sets: + self.log.debug(f"Frame range data {key_sets} " + "has been collected from asset entity.") diff --git a/openpype/plugins/publish/collect_frame_range_data.py b/openpype/plugins/publish/collect_frame_range_data.py deleted file mode 100644 index c25aacd7f3..0000000000 --- a/openpype/plugins/publish/collect_frame_range_data.py +++ /dev/null @@ -1,85 +0,0 @@ -import pyblish.api -import clique -from openpype.pipeline import publish -from openpype.lib import BoolDef - - -class CollectFrameRangeData(pyblish.api.InstancePlugin, - publish.OpenPypePyblishPluginMixin): - """Collect Frame Range data. - """ - - order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Frame Range Data" - families = ["plate", "pointcache", - "vdbcache", "online", - "render"] - hosts = ["traypublisher"] - - def process(self, instance): - repres = instance.data.get("representations") - asset_data = None - if repres: - first_repre = repres[0] - if "ext" not in first_repre: - self.log.warning("Cannot find file extension" - " in representation data") - return - - files = first_repre["files"] - collections, remainder = clique.assemble(files) - if not collections: - # No sequences detected and we can't retrieve - # frame range - self.log.debug( - "No sequences detected in the representation data." - " Skipping collecting frame range data.") - return - collection = collections[0] - repres_frames = list(collection.indexes) - asset_data = { - "frameStart": repres_frames[0], - "frameEnd": repres_frames[-1], - } - - else: - self.log.info( - "No representation data. Using Asset Entity data instead") - asset_doc = instance.data.get("assetEntity") - - attr_values = self.get_attr_values_from_data(instance.data) - if attr_values.get("setAssetFrameRange", True): - if instance.data.get("frameStart") is not None or not asset_doc: # noqa - self.log.debug("Instance has no asset entity set." - " Skipping collecting frame range data.") - return - self.log.debug( - "Falling back to collect frame range" - " data from set asset entity.") - asset_data = asset_doc["data"] - else: - self.log.debug("Skipping collecting frame range data.") - return - - key_sets = [] - for key in ( - "fps", - "frameStart", - "frameEnd", - "handleStart", - "handleEnd" - ): - if key not in instance.data and key in asset_data: - instance.data[key] = asset_data[key] - key_sets.append(key) - - self.log.debug(f"Frame range data {key_sets} " - "has been collected from asset entity.") - - @classmethod - def get_attribute_defs(cls): - return [ - BoolDef("setAssetFrameRange", - label="Set Asset Frame Range", - default=False), - ] diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py new file mode 100644 index 0000000000..5b5c427d9e --- /dev/null +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -0,0 +1,48 @@ +import pyblish.api +import clique + + +class CollectSequenceFrameData(pyblish.api.InstancePlugin): + """Collect Sequence Frame Data + """ + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Sequence Frame Data" + families = ["plate", "pointcache", + "vdbcache", "online", + "render"] + hosts = ["traypublisher"] + + def process(self, instance): + frame_data = self.get_frame_data_from_repre_sequence(instance) + for key, value in frame_data.items(): + if key not in instance.data : + instance.data[key] = value + self.log.debug(f"Frame range data {key} has been collected ") + + + def get_frame_data_from_repre_sequence(self, instance): + repres = instance.data.get("representations") + if repres: + first_repre = repres[0] + if "ext" not in first_repre: + self.log.warning("Cannot find file extension" + " in representation data") + return + + files = first_repre["files"] + collections, remainder = clique.assemble(files) + if not collections: + # No sequences detected and we can't retrieve + # frame range + self.log.debug( + "No sequences detected in the representation data." + " Skipping collecting frame range data.") + return + collection = collections[0] + repres_frames = list(collection.indexes) + + return { + "frameStart": repres_frames[0], + "frameEnd": repres_frames[-1], + } diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index b7a01e584b..dda958ebcd 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -329,7 +329,7 @@ } }, "publish": { - "CollectFrameRangeData": { + "CollectFrameDataFromAssetEntity": { "enabled": true, "optional": true, "active": true diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 4a54c6b4b4..184fc657be 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -350,7 +350,7 @@ "name": "template_validate_plugin", "template_data": [ { - "key": "CollectFrameRangeData", + "key": "CollectFrameDataFromAssetEntity", "label": "Collect frame range from asset entity" }, { From e9c9be0b57750f1fd81c8e70938bf4f6319b2e92 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Jul 2023 17:54:18 +0800 Subject: [PATCH 182/446] hound shut --- openpype/plugins/publish/collect_frame_range_asset_entity.py | 2 +- openpype/plugins/publish/collect_sequence_frame_data.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/collect_frame_range_asset_entity.py b/openpype/plugins/publish/collect_frame_range_asset_entity.py index 2f7570466c..67e7444173 100644 --- a/openpype/plugins/publish/collect_frame_range_asset_entity.py +++ b/openpype/plugins/publish/collect_frame_range_asset_entity.py @@ -3,7 +3,7 @@ from openpype.pipeline import OptionalPyblishPluginMixin class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): + OptionalPyblishPluginMixin): """Collect Frame Range data From Asset Entity """ diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index 5b5c427d9e..0b6c69bd5f 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -16,11 +16,10 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): def process(self, instance): frame_data = self.get_frame_data_from_repre_sequence(instance) for key, value in frame_data.items(): - if key not in instance.data : + if key not in instance.data: instance.data[key] = value self.log.debug(f"Frame range data {key} has been collected ") - def get_frame_data_from_repre_sequence(self, instance): repres = instance.data.get("representations") if repres: From 00070efac77ec8fe92057f23961397c025a9351e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Jul 2023 18:41:38 +0800 Subject: [PATCH 183/446] docstrings update and roy's comment --- .../publish/collect_frame_range_asset_entity.py | 11 +++++++---- .../plugins/publish/collect_sequence_frame_data.py | 8 +++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/collect_frame_range_asset_entity.py b/openpype/plugins/publish/collect_frame_range_asset_entity.py index 67e7444173..ce744e2daf 100644 --- a/openpype/plugins/publish/collect_frame_range_asset_entity.py +++ b/openpype/plugins/publish/collect_frame_range_asset_entity.py @@ -5,6 +5,9 @@ from openpype.pipeline import OptionalPyblishPluginMixin class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Collect Frame Range data From Asset Entity + + Frame range data will only be collected if the keys + are not yet collected for the instance. """ order = pyblish.api.CollectorOrder + 0.3 @@ -28,12 +31,12 @@ class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin, ): if key not in instance.data: missing_keys.append(key) - key_sets = [] + keys_set = [] for key in missing_keys: asset_data = instance.data["assetEntity"]["data"] if key in asset_data: instance.data[key] = asset_data[key] - key_sets.append(key) - if key_sets: - self.log.debug(f"Frame range data {key_sets} " + keys_set.append(key) + if keys_set: + self.log.debug(f"Frame range data {keys_set} " "has been collected from asset entity.") diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index 0b6c69bd5f..c200b245e9 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -4,6 +4,9 @@ import clique class CollectSequenceFrameData(pyblish.api.InstancePlugin): """Collect Sequence Frame Data + If the representation includes files with frame numbers, + then set `frameStart` and `frameEnd` for the instance to the + start and end frame respectively """ order = pyblish.api.CollectorOrder + 0.2 @@ -15,10 +18,13 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): def process(self, instance): frame_data = self.get_frame_data_from_repre_sequence(instance) + if not frame_data: + # if no dict data skip collecting the frame range data + return for key, value in frame_data.items(): if key not in instance.data: instance.data[key] = value - self.log.debug(f"Frame range data {key} has been collected ") + self.log.debug(f"Collected Frame range data '{key}':{value} ") def get_frame_data_from_repre_sequence(self, instance): repres = instance.data.get("representations") From 3d336717237ed9c3bb0df9b27732fa52872782e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Jul 2023 14:08:10 +0200 Subject: [PATCH 184/446] Refactor imports to `lib.get_reference_node` since the other function is deprecated --- openpype/hosts/maya/plugins/inventory/import_reference.py | 2 +- openpype/hosts/maya/plugins/load/load_look.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/import_reference.py b/openpype/hosts/maya/plugins/inventory/import_reference.py index afb1e0e17f..ecc424209d 100644 --- a/openpype/hosts/maya/plugins/inventory/import_reference.py +++ b/openpype/hosts/maya/plugins/inventory/import_reference.py @@ -1,7 +1,7 @@ from maya import cmds from openpype.pipeline import InventoryAction -from openpype.hosts.maya.api.plugin import get_reference_node +from openpype.hosts.maya.api.lib import get_reference_node class ImportReference(InventoryAction): diff --git a/openpype/hosts/maya/plugins/load/load_look.py b/openpype/hosts/maya/plugins/load/load_look.py index 8f3e017658..cd1f40c916 100644 --- a/openpype/hosts/maya/plugins/load/load_look.py +++ b/openpype/hosts/maya/plugins/load/load_look.py @@ -14,7 +14,7 @@ import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api import lib from openpype.widgets.message_window import ScrollMessageBox -from openpype.hosts.maya.api.plugin import get_reference_node +from openpype.hosts.maya.api.lib import get_reference_node class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): From 0f015a6f9207d6f6c65d5319bcdc12a7912982a6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 7 Jul 2023 20:30:55 +0800 Subject: [PATCH 185/446] jakub's comment --- openpype/plugins/load/delivery.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 3d4b90686e..b78666528c 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -97,9 +97,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): renumber_frame = QtWidgets.QCheckBox() - renumber_frame_value = QtWidgets.QSpinBox() + first_frame_start = QtWidgets.QSpinBox() max_int = (1 << 32) // 2 - renumber_frame_value.setRange(-max_int, max_int - 1) + first_frame_start.setRange(0, max_int - 1) root_line_edit = QtWidgets.QLineEdit() @@ -125,7 +125,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): input_layout.addRow("Delivery template", dropdown) input_layout.addRow("Template value", template_label) input_layout.addRow("Renumber Frame", renumber_frame) - input_layout.addRow("Renumber start frame", renumber_frame_value) + input_layout.addRow("Renumber start frame", first_frame_start) input_layout.addRow("Root", root_line_edit) input_layout.addRow("Representations", repre_checkboxes_layout) @@ -153,7 +153,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self.selected_label = selected_label self.template_label = template_label self.dropdown = dropdown - self.renumber_frame_value = renumber_frame_value + self.first_frame_start = first_frame_start self.renumber_frame = renumber_frame self.root_line_edit = root_line_edit self.progress_bar = progress_bar @@ -192,7 +192,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): template_name = self.dropdown.currentText() format_dict = get_format_dict(self.anatomy, self.root_line_edit.text()) renumber_frame = self.renumber_frame.isChecked() - frame_offset = self.renumber_frame_value.value() + frame_offset = self.first_frame_start.value() for repre in self._representations: if repre["name"] not in selected_repres: continue @@ -223,13 +223,6 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self.log ] - if renumber_frame: - optional_args = [ - renumber_frame, - frame_offset - ] - args.extend(optional_args) - if repre.get("files"): src_paths = [] for repre_file in repre["files"]: @@ -241,8 +234,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): frames.discard(None) first_frame = None if frames: - first_frame = next(iter(frames)) - + first_frame = min(frames) for src_path, frame in sources_and_frames.items(): args[0] = src_path @@ -253,8 +245,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): # - '0' for first frame offset = frame_offset - int(first_frame) # Add offset to new frame start - frame = int(frame) - dst_frame = frame + offset + dst_frame = int(frame) + offset if dst_frame < 0: msg = "Renumber frame has a smaller number than original frame" # noqa report_items[msg].append(src_path) @@ -274,7 +265,6 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if frame: repre["context"]["frame"] = len(str(frame)) * "#" - if not frame: new_report_items, uploaded = deliver_single_file(*args) else: new_report_items, uploaded = deliver_sequence(*args) From eee670fc0f9278b648284a7b4d0047a32eb6c316 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 7 Jul 2023 20:33:14 +0800 Subject: [PATCH 186/446] jakub's comment --- openpype/pipeline/delivery.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index fe22f7401c..1bb19498da 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -120,9 +120,7 @@ def deliver_single_file( anatomy_data, format_dict, report_items, - log, - has_renumbered_frame=False, - new_frame_start=0 + log ): """Copy single file to calculated path based on template From a687cc068d63d51306303073c1a1ec93645ad53e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 7 Jul 2023 20:37:26 +0800 Subject: [PATCH 187/446] Jakub's comment --- openpype/plugins/load/delivery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index b78666528c..3b493989bd 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -255,8 +255,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): frame = dst_frame if frame is not None: - if frame: - anatomy_data["frame"] = frame + anatomy_data["frame"] = frame new_report_items, uploaded = deliver_single_file(*args) report_items.update(new_report_items) self._update_progress(uploaded) @@ -265,6 +264,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if frame: repre["context"]["frame"] = len(str(frame)) * "#" + if not frame: new_report_items, uploaded = deliver_single_file(*args) else: new_report_items, uploaded = deliver_sequence(*args) From 9975b87a9b3501a6c573ae75d6eba3d1dc0064c5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 10 Jul 2023 19:01:17 +0200 Subject: [PATCH 188/446] :recycle: move type family to collector --- .../houdini/plugins/create/create_bgeo.py | 15 +++++++------ .../plugins/create/create_pointcache.py | 4 ---- .../publish/collect_pointcache_type.py | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index 6edc2e76d2..118d2f4391 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache bgeo files.""" from openpype.hosts.houdini.api import plugin -from openpype.pipeline import CreatedInstance +from openpype.pipeline import CreatedInstance, CreatorError from openpype.lib import EnumDef @@ -19,10 +19,6 @@ class CreateBGEO(plugin.HoudiniCreator): instance_data.update({"node_type": "geometry"}) - if not instance_data.get("families"): - instance_data["families"] = [] - instance_data["families"] += ["bgeo"] - instance = super(CreateBGEO, self).create( subset_name, instance_data, @@ -40,6 +36,7 @@ class CreateBGEO(plugin.HoudiniCreator): "sopoutput": file_path } + instance_node.parm("trange").set(1) if self.selected_nodes: # if selection is on SOP level, use it if isinstance(self.selected_nodes[0], hou.SopNode): @@ -50,11 +47,17 @@ class CreateBGEO(plugin.HoudiniCreator): child for child in self.selected_nodes[0].children() if child.type().name() == "output" ] + if not outputs: + instance_node.setParms(parms) + raise CreatorError(( + "Missing output node in SOP level for the selection. " + "Please select correct SOP path in created instance." + )) outputs.sort(key=lambda output: output.evalParm("outputidx")) parms["soppath"] = outputs[0].path() instance_node.setParms(parms) - instance_node.parm("trange").set(1) + def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 3e3df45e5b..554d5f2016 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -16,10 +16,6 @@ class CreatePointCache(plugin.HoudiniCreator): instance_data.pop("active", None) instance_data.update({"node_type": "alembic"}) - if not instance_data.get("families"): - instance_data["families"] = [] - instance_data["families"] += ["abc"] - instance = super(CreatePointCache, self).create( subset_name, instance_data, diff --git a/openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py b/openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py new file mode 100644 index 0000000000..6c527377e0 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py @@ -0,0 +1,21 @@ +"""Collector for pointcache types. + +This will add additional family to pointcache instance based on +the creator_identifier parameter. +""" +import pyblish.api + + +class CollectPointcacheType(pyblish.api.InstancePlugin): + """Collect data type for pointcache instance.""" + + order = pyblish.api.CollectorOrder + hosts = ["houdini"] + families = ["pointcache"] + label = "Collect type of pointcache" + + def process(self, instance): + if instance.data["creator_identifier"] == "io.openpype.creators.houdini.bgeo": # noqa: E501 + instance.data["families"] += ["bgeo"] + elif instance.data["creator_identifier"] == "io.openpype.creators.houdini.alembic": # noqa: E501 + instance.data["families"] += ["abc"] From 7f508d7837dfbe0eb4ea4787ddf971a7913149b3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 10 Jul 2023 19:01:46 +0200 Subject: [PATCH 189/446] :recycle: make code simpler --- openpype/hosts/houdini/plugins/publish/extract_bgeo.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py index 4d876cb181..c9625ec880 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -22,11 +22,9 @@ class ExtractBGEO(publish.Extractor): # Get the filename from the filename parameter output = ropnode.evalParm("sopoutput") - staging_dir = os.path.dirname(output) + staging_dir, file_name = os.path.split(output) instance.data["stagingDir"] = staging_dir - file_name = os.path.basename(output) - # We run the render self.log.info("Writing bgeo files '{}' to '{}'.".format( file_name, staging_dir)) From ca7f75ec6f2123208b80bdefe10a332da6a63a46 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 10 Jul 2023 19:02:27 +0200 Subject: [PATCH 190/446] :memo: fix docs --- website/docs/artist_hosts_houdini.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/docs/artist_hosts_houdini.md b/website/docs/artist_hosts_houdini.md index 8a9d2e1f93..940d5ac351 100644 --- a/website/docs/artist_hosts_houdini.md +++ b/website/docs/artist_hosts_houdini.md @@ -139,7 +139,7 @@ There is a simple support for publishing and loading **BGEO** files in all suppo ### Creating BGEO instances -Just select your SOP node to be exported as BGEO. If your selection is object level, OpenPype will try to find there is `output` node inside, the one with the lowest index will be used: +Select your SOP node to be exported as BGEO. If your selection is in the object level, OpenPype will try to find if there is an `output` node inside, the one with the lowest index will be used: ![BGEO output node](assets/houdini_bgeo_output_node.png) @@ -147,8 +147,7 @@ Then you can open Publisher, in Create you select **BGEO PointCache**: ![BGEO Publisher](assets/houdini_bgeo-publisher.png) -You can select compression type and if the current selection should be connected to ROPs SOP path parameter. Publishing -will produce sequence of files based on your timeline settings. +You can select compression type and if the current selection should be connected to ROPs SOP path parameter. Publishing will produce sequence of files based on your timeline settings. ### Loading BGEO From a3da1a4c02aed5265adc78a756bc66ed327e7386 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 10 Jul 2023 19:07:40 +0200 Subject: [PATCH 191/446] :dog: fix hound --- openpype/hosts/houdini/plugins/create/create_bgeo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index 118d2f4391..a1101fd045 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -58,7 +58,6 @@ class CreateBGEO(plugin.HoudiniCreator): instance_node.setParms(parms) - def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() bgeo_enum = [ From 726f6f3fdb3dd704f0144d061f6b0ca7646249fe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jul 2023 15:47:43 +0200 Subject: [PATCH 192/446] Nuke: auto apply all settings after template build --- openpype/hosts/nuke/api/workfile_template_builder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 2384e8eca1..a19cb9dfea 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -25,7 +25,8 @@ from .lib import ( select_nodes, duplicate_node, node_tempfile, - get_main_window + get_main_window, + WorkfileSettings, ) PLACEHOLDER_SET = "PLACEHOLDERS_SET" @@ -955,6 +956,9 @@ def build_workfile_template(*args, **kwargs): builder = NukeTemplateBuilder(registered_host()) builder.build_template(*args, **kwargs) + # set all settings to shot context default + WorkfileSettings().set_context_settings() + def update_workfile_template(*args): builder = NukeTemplateBuilder(registered_host()) From 7992ba69692d211324c205be0b11080607e63284 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Apr 2023 11:43:33 +0200 Subject: [PATCH 193/446] :bug: fix error reports in silent mode --- start.py | 51 ++++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/start.py b/start.py index 91e5c29a53..6b2cbbd907 100644 --- a/start.py +++ b/start.py @@ -140,8 +140,8 @@ import certifi # noqa: E402 if sys.__stdout__: term = blessed.Terminal() - def _print(message: str): - if silent_mode: + def _print(message: str, force=False): + if silent_mode and not force: return if message.startswith("!!! "): print(f'{term.orangered2("!!! ")}{message[4:]}') @@ -507,8 +507,8 @@ def _process_arguments() -> tuple: not use_version_value or not use_version_value.startswith("=") ): - _print("!!! Please use option --use-version like:") - _print(" --use-version=3.0.0") + _print("!!! Please use option --use-version like:", True) + _print(" --use-version=3.0.0", True) sys.exit(1) version_str = use_version_value[1:] @@ -525,14 +525,14 @@ def _process_arguments() -> tuple: break if use_version is None: - _print("!!! Requested version isn't in correct format.") + _print("!!! Requested version isn't in correct format.", True) _print((" Use --list-versions to find out" - " proper version string.")) + " proper version string."), True) sys.exit(1) if arg == "--validate-version": - _print("!!! Please use option --validate-version like:") - _print(" --validate-version=3.0.0") + _print("!!! Please use option --validate-version like:", True) + _print(" --validate-version=3.0.0", True) sys.exit(1) if arg.startswith("--validate-version="): @@ -543,9 +543,9 @@ def _process_arguments() -> tuple: sys.argv.remove(arg) commands.append("validate") else: - _print("!!! Requested version isn't in correct format.") + _print("!!! Requested version isn't in correct format.", True) _print((" Use --list-versions to find out" - " proper version string.")) + " proper version string."), True) sys.exit(1) if "--list-versions" in sys.argv: @@ -556,7 +556,7 @@ def _process_arguments() -> tuple: # this is helper to run igniter before anything else if "igniter" in sys.argv: if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": - _print("!!! Cannot open Igniter dialog in headless mode.") + _print("!!! Cannot open Igniter dialog in headless mode.", True) sys.exit(1) return_code = igniter.open_dialog() @@ -606,9 +606,9 @@ def _determine_mongodb() -> str: if not openpype_mongo: _print("*** No DB connection string specified.") if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": - _print("!!! Cannot open Igniter dialog in headless mode.") - _print( - "!!! Please use `OPENPYPE_MONGO` to specify server address.") + _print("!!! Cannot open Igniter dialog in headless mode.", True) + _print(("!!! Please use `OPENPYPE_MONGO` to specify " + "server address."), True) sys.exit(1) _print("--- launching setup UI ...") @@ -783,7 +783,7 @@ def _find_frozen_openpype(use_version: str = None, try: version_path = bootstrap.extract_openpype(openpype_version) except OSError as e: - _print("!!! failed: {}".format(str(e))) + _print("!!! failed: {}".format(str(e)), True) sys.exit(1) else: # cleanup zip after extraction @@ -899,7 +899,7 @@ def _boot_validate_versions(use_version, local_version): v: OpenPypeVersion found = [v for v in openpype_versions if str(v) == use_version] if not found: - _print(f"!!! Version [ {use_version} ] not found.") + _print(f"!!! Version [ {use_version} ] not found.", True) list_versions(openpype_versions, local_version) sys.exit(1) @@ -908,7 +908,8 @@ def _boot_validate_versions(use_version, local_version): use_version, openpype_versions ) valid, message = bootstrap.validate_openpype_version(version_path) - _print(f'{">>> " if valid else "!!! "}{message}') + _print(f'{">>> " if valid else "!!! "}{message}', not valid) + return valid def _boot_print_versions(openpype_root): @@ -935,7 +936,7 @@ def _boot_print_versions(openpype_root): def _boot_handle_missing_version(local_version, message): - _print(message) + _print(message, True) if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": openpype_versions = bootstrap.find_openpype( include_zips=True) @@ -983,7 +984,7 @@ def boot(): openpype_mongo = _determine_mongodb() except RuntimeError as e: # without mongodb url we are done for. - _print(f"!!! {e}") + _print(f"!!! {e}", True) sys.exit(1) os.environ["OPENPYPE_MONGO"] = openpype_mongo @@ -1024,8 +1025,8 @@ def boot(): local_version = OpenPypeVersion.get_installed_version_str() if "validate" in commands: - _boot_validate_versions(use_version, local_version) - sys.exit(1) + valid = _boot_validate_versions(use_version, local_version) + sys.exit(1 if not valid else 0) if not openpype_path: _print("*** Cannot get OpenPype path from database.") @@ -1035,7 +1036,7 @@ def boot(): if "print_versions" in commands: _boot_print_versions(OPENPYPE_ROOT) - sys.exit(1) + sys.exit(0) # ------------------------------------------------------------------------ # Find OpenPype versions @@ -1052,13 +1053,13 @@ def boot(): except RuntimeError as e: # no version to run - _print(f"!!! {e}") + _print(f"!!! {e}", True) sys.exit(1) # validate version _print(f">>> Validating version [ {str(version_path)} ]") result = bootstrap.validate_openpype_version(version_path) if not result[0]: - _print(f"!!! Invalid version: {result[1]}") + _print(f"!!! Invalid version: {result[1]}", True) sys.exit(1) _print("--- version is valid") else: @@ -1126,7 +1127,7 @@ def boot(): cli.main(obj={}, prog_name="openpype") except Exception: # noqa exc_info = sys.exc_info() - _print("!!! OpenPype crashed:") + _print("!!! OpenPype crashed:", True) traceback.print_exception(*exc_info) sys.exit(1) From e42918997ee3c751702bb7913dc255ef533e3a1f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 13:57:03 +0100 Subject: [PATCH 194/446] fix possible issues with destination drive path --- start.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/start.py b/start.py index 6b2cbbd907..4849a241d2 100644 --- a/start.py +++ b/start.py @@ -348,8 +348,15 @@ def run_disk_mapping_commands(settings): mappings = disk_mapping.get(low_platform) or [] for source, destination in mappings: - destination = destination.rstrip('/') - source = source.rstrip('/') + if low_platform == "windows": + destination = destination.replace("/", "\\").rstrip("\\") + source = source.replace("/", "\\").rstrip("\\") + # Add slash after ':' ('G:' -> 'G:\') + if destination.endswith(":"): + destination += "\\" + else: + destination = destination.rstrip("/") + source = source.rstrip("/") if low_platform == "darwin": scr = f'do shell script "ln -s {source} {destination}" with administrator privileges' # noqa From 2951b2690edb3f53c5c3c6c5c344251a1e9c3bf8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 30 Jan 2023 12:23:53 +0100 Subject: [PATCH 195/446] :construction: docs reborn --- docs/source/conf.py | 77 +++++++-- docs/source/igniter.bootstrap_repos.rst | 7 - docs/source/igniter.install_dialog.rst | 7 - docs/source/igniter.install_thread.rst | 7 - docs/source/igniter.rst | 42 ----- docs/source/igniter.tools.rst | 7 - docs/source/pype.action.rst | 7 - docs/source/pype.api.rst | 7 - docs/source/pype.cli.rst | 7 - docs/source/pype.hosts.aftereffects.rst | 7 - docs/source/pype.hosts.blender.action.rst | 7 - docs/source/pype.hosts.blender.plugin.rst | 7 - docs/source/pype.hosts.blender.rst | 26 ---- docs/source/pype.hosts.celaction.cli.rst | 7 - docs/source/pype.hosts.celaction.rst | 18 --- docs/source/pype.hosts.fusion.lib.rst | 7 - docs/source/pype.hosts.fusion.menu.rst | 7 - docs/source/pype.hosts.fusion.pipeline.rst | 7 - docs/source/pype.hosts.fusion.rst | 26 ---- ...s.fusion.scripts.duplicate_with_inputs.rst | 7 - ...osts.fusion.scripts.fusion_switch_shot.rst | 7 - docs/source/pype.hosts.fusion.scripts.rst | 26 ---- ...pe.hosts.fusion.scripts.set_rendermode.rst | 7 - docs/source/pype.hosts.fusion.utils.rst | 7 - docs/source/pype.hosts.harmony.rst | 7 - docs/source/pype.hosts.hiero.events.rst | 7 - docs/source/pype.hosts.hiero.lib.rst | 7 - docs/source/pype.hosts.hiero.menu.rst | 7 - docs/source/pype.hosts.hiero.rst | 19 --- docs/source/pype.hosts.hiero.tags.rst | 7 - docs/source/pype.hosts.hiero.workio.rst | 7 - docs/source/pype.hosts.houdini.lib.rst | 7 - docs/source/pype.hosts.houdini.rst | 18 --- docs/source/pype.hosts.maya.action.rst | 7 - docs/source/pype.hosts.maya.customize.rst | 7 - .../source/pype.hosts.maya.expected_files.rst | 7 - docs/source/pype.hosts.maya.lib.rst | 7 - docs/source/pype.hosts.maya.menu.rst | 7 - docs/source/pype.hosts.maya.plugin.rst | 7 - docs/source/pype.hosts.maya.rst | 58 ------- docs/source/pype.hosts.nuke.actions.rst | 7 - docs/source/pype.hosts.nuke.lib.rst | 7 - docs/source/pype.hosts.nuke.menu.rst | 7 - docs/source/pype.hosts.nuke.plugin.rst | 7 - docs/source/pype.hosts.nuke.presets.rst | 7 - docs/source/pype.hosts.nuke.rst | 58 ------- docs/source/pype.hosts.nuke.utils.rst | 7 - docs/source/pype.hosts.nukestudio.rst | 50 ------ docs/source/pype.hosts.photoshop.rst | 7 - docs/source/pype.hosts.premiere.lib.rst | 7 - docs/source/pype.hosts.premiere.rst | 18 --- docs/source/pype.hosts.resolve.action.rst | 7 - docs/source/pype.hosts.resolve.lib.rst | 7 - docs/source/pype.hosts.resolve.menu.rst | 7 - ...pype.hosts.resolve.otio.davinci_export.rst | 7 - ...pype.hosts.resolve.otio.davinci_import.rst | 7 - docs/source/pype.hosts.resolve.otio.rst | 17 -- docs/source/pype.hosts.resolve.otio.utils.rst | 7 - docs/source/pype.hosts.resolve.pipeline.rst | 7 - docs/source/pype.hosts.resolve.plugin.rst | 7 - .../pype.hosts.resolve.preload_console.rst | 7 - docs/source/pype.hosts.resolve.rst | 74 --------- .../pype.hosts.resolve.todo-rendering.rst | 7 - docs/source/pype.hosts.resolve.utils.rst | 7 - docs/source/pype.hosts.resolve.workio.rst | 7 - docs/source/pype.hosts.rst | 26 ---- docs/source/pype.hosts.tvpaint.api.rst | 7 - docs/source/pype.hosts.tvpaint.rst | 15 -- docs/source/pype.hosts.unreal.lib.rst | 7 - docs/source/pype.hosts.unreal.plugin.rst | 7 - docs/source/pype.hosts.unreal.rst | 26 ---- docs/source/pype.launcher_actions.rst | 7 - .../pype.lib.abstract_collect_render.rst | 7 - .../pype.lib.abstract_expected_files.rst | 7 - docs/source/pype.lib.abstract_metaplugins.rst | 7 - .../pype.lib.abstract_submit_deadline.rst | 7 - docs/source/pype.lib.anatomy.rst | 7 - docs/source/pype.lib.applications.rst | 7 - docs/source/pype.lib.avalon_context.rst | 7 - docs/source/pype.lib.config.rst | 7 - docs/source/pype.lib.deprecated.rst | 7 - docs/source/pype.lib.editorial.rst | 7 - docs/source/pype.lib.env_tools.rst | 7 - docs/source/pype.lib.execute.rst | 7 - docs/source/pype.lib.ffmpeg_utils.rst | 7 - docs/source/pype.lib.git_progress.rst | 7 - docs/source/pype.lib.log.rst | 7 - docs/source/pype.lib.mongo.rst | 7 - docs/source/pype.lib.path_tools.rst | 7 - docs/source/pype.lib.plugin_tools.rst | 7 - docs/source/pype.lib.profiling.rst | 7 - docs/source/pype.lib.python_module_tools.rst | 7 - docs/source/pype.lib.rst | 90 ----------- docs/source/pype.lib.terminal.rst | 7 - docs/source/pype.lib.terminal_splash.rst | 7 - docs/source/pype.lib.user_settings.rst | 7 - ...s.adobe_communicator.adobe_comunicator.rst | 7 - ...modules.adobe_communicator.lib.publish.rst | 7 - ...odules.adobe_communicator.lib.rest_api.rst | 7 - .../pype.modules.adobe_communicator.lib.rst | 26 ---- .../pype.modules.adobe_communicator.rst | 26 ---- .../pype.modules.avalon_apps.avalon_app.rst | 7 - .../pype.modules.avalon_apps.rest_api.rst | 7 - docs/source/pype.modules.avalon_apps.rst | 26 ---- docs/source/pype.modules.base.rst | 7 - .../source/pype.modules.clockify.clockify.rst | 7 - .../pype.modules.clockify.clockify_api.rst | 7 - .../pype.modules.clockify.clockify_module.rst | 7 - .../pype.modules.clockify.constants.rst | 7 - docs/source/pype.modules.clockify.rst | 42 ----- docs/source/pype.modules.clockify.widgets.rst | 7 - .../pype.modules.deadline.deadline_module.rst | 7 - docs/source/pype.modules.deadline.rst | 15 -- .../pype.modules.ftrack.ftrack_module.rst | 7 - ...rack.ftrack_server.custom_db_connector.rst | 7 - ....ftrack.ftrack_server.event_server_cli.rst | 7 - ...les.ftrack.ftrack_server.ftrack_server.rst | 7 - .../pype.modules.ftrack.ftrack_server.lib.rst | 7 - .../pype.modules.ftrack.ftrack_server.rst | 90 ----------- ...les.ftrack.ftrack_server.socket_thread.rst | 7 - ...rack.ftrack_server.sub_event_processor.rst | 7 - ....ftrack.ftrack_server.sub_event_status.rst | 7 - ....ftrack.ftrack_server.sub_event_storer.rst | 7 - ...ftrack.ftrack_server.sub_legacy_server.rst | 7 - ...s.ftrack.ftrack_server.sub_user_server.rst | 7 - .../pype.modules.ftrack.lib.avalon_sync.rst | 7 - .../pype.modules.ftrack.lib.credentials.rst | 7 - ...dules.ftrack.lib.ftrack_action_handler.rst | 7 - ....modules.ftrack.lib.ftrack_app_handler.rst | 7 - ...modules.ftrack.lib.ftrack_base_handler.rst | 7 - ...odules.ftrack.lib.ftrack_event_handler.rst | 7 - docs/source/pype.modules.ftrack.lib.rst | 58 ------- .../pype.modules.ftrack.lib.settings.rst | 7 - docs/source/pype.modules.ftrack.rst | 17 -- ...pype.modules.ftrack.tray.ftrack_module.rst | 7 - .../pype.modules.ftrack.tray.ftrack_tray.rst | 7 - .../pype.modules.ftrack.tray.login_dialog.rst | 7 - .../pype.modules.ftrack.tray.login_tools.rst | 7 - docs/source/pype.modules.ftrack.tray.rst | 34 ---- ...pype.modules.idle_manager.idle_manager.rst | 7 - docs/source/pype.modules.idle_manager.rst | 18 --- docs/source/pype.modules.launcher_action.rst | 7 - ...ype.modules.log_viewer.log_view_module.rst | 7 - docs/source/pype.modules.log_viewer.rst | 23 --- .../pype.modules.log_viewer.tray.app.rst | 7 - .../pype.modules.log_viewer.tray.models.rst | 7 - docs/source/pype.modules.log_viewer.tray.rst | 17 -- .../pype.modules.log_viewer.tray.widgets.rst | 7 - docs/source/pype.modules.muster.muster.rst | 7 - docs/source/pype.modules.muster.rst | 26 ---- .../pype.modules.muster.widget_login.rst | 7 - .../pype.modules.rest_api.base_class.rst | 7 - .../pype.modules.rest_api.lib.exceptions.rst | 7 - .../pype.modules.rest_api.lib.factory.rst | 7 - .../pype.modules.rest_api.lib.handler.rst | 7 - docs/source/pype.modules.rest_api.lib.lib.rst | 7 - docs/source/pype.modules.rest_api.lib.rst | 42 ----- .../source/pype.modules.rest_api.rest_api.rst | 7 - docs/source/pype.modules.rest_api.rst | 34 ---- docs/source/pype.modules.rst | 36 ----- docs/source/pype.modules.settings_action.rst | 7 - .../source/pype.modules.standalonepublish.rst | 18 --- ...dalonepublish.standalonepublish_module.rst | 7 - .../pype.modules.standalonepublish_action.rst | 7 - docs/source/pype.modules.sync_server.rst | 16 -- .../pype.modules.sync_server.sync_server.rst | 7 - .../source/pype.modules.sync_server.utils.rst | 7 - docs/source/pype.modules.timers_manager.rst | 26 ---- ....modules.timers_manager.timers_manager.rst | 7 - ...odules.timers_manager.widget_user_idle.rst | 7 - docs/source/pype.modules.user.rst | 26 ---- docs/source/pype.modules.user.user_module.rst | 7 - docs/source/pype.modules.user.widget_user.rst | 7 - ...es.websocket_server.hosts.aftereffects.rst | 7 - ....websocket_server.hosts.external_app_1.rst | 7 - ...dules.websocket_server.hosts.photoshop.rst | 7 - .../pype.modules.websocket_server.hosts.rst | 26 ---- docs/source/pype.modules.websocket_server.rst | 26 ---- ...ules.websocket_server.websocket_server.rst | 7 - docs/source/pype.modules_manager.rst | 7 - docs/source/pype.plugin.rst | 7 - ...plugins.maya.publish.collect_animation.rst | 7 - .../pype.plugins.maya.publish.collect_ass.rst | 7 - ....plugins.maya.publish.collect_assembly.rst | 7 - ...maya.publish.collect_file_dependencies.rst | 7 - ...ins.maya.publish.collect_ftrack_family.rst | 7 - ...e.plugins.maya.publish.collect_history.rst | 7 - ...plugins.maya.publish.collect_instances.rst | 7 - ...pype.plugins.maya.publish.collect_look.rst | 7 - ...lugins.maya.publish.collect_maya_units.rst | 7 - ...ns.maya.publish.collect_maya_workspace.rst | 7 - ...plugins.maya.publish.collect_mayaascii.rst | 7 - ...ype.plugins.maya.publish.collect_model.rst | 7 - ...ins.maya.publish.collect_remove_marked.rst | 7 - ...pe.plugins.maya.publish.collect_render.rst | 7 - ...maya.publish.collect_render_layer_aovs.rst | 7 - ...maya.publish.collect_renderable_camera.rst | 7 - ...pe.plugins.maya.publish.collect_review.rst | 7 - .../pype.plugins.maya.publish.collect_rig.rst | 7 - ...ype.plugins.maya.publish.collect_scene.rst | 7 - ...maya.publish.collect_unreal_staticmesh.rst | 7 - ...ins.maya.publish.collect_workscene_fps.rst | 7 - ...lugins.maya.publish.collect_yeti_cache.rst | 7 - ....plugins.maya.publish.collect_yeti_rig.rst | 7 - ....maya.publish.determine_future_version.rst | 7 - ...plugins.maya.publish.extract_animation.rst | 7 - .../pype.plugins.maya.publish.extract_ass.rst | 7 - ....plugins.maya.publish.extract_assembly.rst | 7 - ....plugins.maya.publish.extract_assproxy.rst | 7 - ...ns.maya.publish.extract_camera_alembic.rst | 7 - ....maya.publish.extract_camera_mayaScene.rst | 7 - .../pype.plugins.maya.publish.extract_fbx.rst | 7 - ...pype.plugins.maya.publish.extract_look.rst | 7 - ...ns.maya.publish.extract_maya_scene_raw.rst | 7 - ...ype.plugins.maya.publish.extract_model.rst | 7 - ...plugins.maya.publish.extract_playblast.rst | 7 - ...lugins.maya.publish.extract_pointcache.rst | 7 - ...ugins.maya.publish.extract_rendersetup.rst | 7 - .../pype.plugins.maya.publish.extract_rig.rst | 7 - ...plugins.maya.publish.extract_thumbnail.rst | 7 - ...plugins.maya.publish.extract_vrayproxy.rst | 7 - ...lugins.maya.publish.extract_yeti_cache.rst | 7 - ....plugins.maya.publish.extract_yeti_rig.rst | 7 - ...ublish.increment_current_file_deadline.rst | 7 - docs/source/pype.plugins.maya.publish.rst | 146 ------------------ .../pype.plugins.maya.publish.save_scene.rst | 7 - ...gins.maya.publish.submit_maya_deadline.rst | 7 - ...lugins.maya.publish.submit_maya_muster.rst | 7 - ...aya.publish.validate_animation_content.rst | 7 - ...ate_animation_out_set_related_node_ids.rst | 7 - ...ya.publish.validate_ass_relative_paths.rst | 7 - ...ns.maya.publish.validate_assembly_name.rst | 7 - ...a.publish.validate_assembly_namespaces.rst | 7 - ...a.publish.validate_assembly_transforms.rst | 7 - ...ugins.maya.publish.validate_attributes.rst | 7 - ...aya.publish.validate_camera_attributes.rst | 7 - ....maya.publish.validate_camera_contents.rst | 7 - ...ugins.maya.publish.validate_color_sets.rst | 7 - ...alidate_current_renderlayer_renderable.rst | 7 - ...a.publish.validate_deadline_connection.rst | 7 - ...gins.maya.publish.validate_frame_range.rst | 7 - ....publish.validate_instance_has_members.rst | 7 - ....maya.publish.validate_instance_subset.rst | 7 - ...aya.publish.validate_instancer_content.rst | 7 - ...ublish.validate_instancer_frame_ranges.rst | 7 - ...ns.maya.publish.validate_joints_hidden.rst | 7 - ...ns.maya.publish.validate_look_contents.rst | 7 - ...idate_look_default_shaders_connections.rst | 7 - ...blish.validate_look_id_reference_edits.rst | 7 - ...a.publish.validate_look_members_unique.rst | 7 - ...blish.validate_look_no_default_shaders.rst | 7 - ...lugins.maya.publish.validate_look_sets.rst | 7 - ...ya.publish.validate_look_shading_group.rst | 7 - ...ya.publish.validate_look_single_shader.rst | 7 - ...ugins.maya.publish.validate_maya_units.rst | 7 - ...ublish.validate_mesh_arnold_attributes.rst | 7 - ...gins.maya.publish.validate_mesh_has_uv.rst | 7 - ...aya.publish.validate_mesh_lamina_faces.rst | 7 - ...ublish.validate_mesh_no_negative_scale.rst | 7 - ...aya.publish.validate_mesh_non_manifold.rst | 7 - ...ya.publish.validate_mesh_non_zero_edge.rst | 7 - ...publish.validate_mesh_normals_unlocked.rst | 7 - ....publish.validate_mesh_overlapping_uvs.rst | 7 - ...blish.validate_mesh_shader_connections.rst | 7 - ...ya.publish.validate_mesh_single_uv_set.rst | 7 - ...maya.publish.validate_mesh_uv_set_map1.rst | 7 - ...lish.validate_mesh_vertices_have_edges.rst | 7 - ...ns.maya.publish.validate_model_content.rst | 7 - ...ugins.maya.publish.validate_model_name.rst | 7 - ...aya.publish.validate_muster_connection.rst | 7 - ...ins.maya.publish.validate_no_animation.rst | 7 - ...aya.publish.validate_no_default_camera.rst | 7 - ...ins.maya.publish.validate_no_namespace.rst | 7 - ...ya.publish.validate_no_null_transforms.rst | 7 - ...maya.publish.validate_no_unknown_nodes.rst | 7 - ...gins.maya.publish.validate_no_vraymesh.rst | 7 - ...plugins.maya.publish.validate_node_ids.rst | 7 - ...lish.validate_node_ids_deformed_shapes.rst | 7 - ....publish.validate_node_ids_in_database.rst | 7 - ...maya.publish.validate_node_ids_related.rst | 7 - ....maya.publish.validate_node_ids_unique.rst | 7 - ...maya.publish.validate_node_no_ghosting.rst | 7 - ...aya.publish.validate_render_image_rule.rst | 7 - ...ish.validate_render_no_default_cameras.rst | 7 - ....publish.validate_render_single_camera.rst | 7 - ...maya.publish.validate_renderlayer_aovs.rst | 7 - ...s.maya.publish.validate_rendersettings.rst | 7 - ...lugins.maya.publish.validate_resources.rst | 7 - ...ins.maya.publish.validate_rig_contents.rst | 7 - ....maya.publish.validate_rig_controllers.rst | 7 - ...date_rig_controllers_arnold_attributes.rst | 7 - ....publish.validate_rig_out_set_node_ids.rst | 7 - ...s.maya.publish.validate_rig_output_ids.rst | 7 - ...a.publish.validate_scene_set_workspace.rst | 7 - ...gins.maya.publish.validate_shader_name.rst | 7 - ...a.publish.validate_shape_default_names.rst | 7 - ...ya.publish.validate_shape_render_stats.rst | 7 - ....maya.publish.validate_single_assembly.rst | 7 - ...lish.validate_skinCluster_deformer_set.rst | 7 - ...lugins.maya.publish.validate_step_size.rst | 7 - ...blish.validate_transform_naming_suffix.rst | 7 - ...s.maya.publish.validate_transform_zero.rst | 7 - ....maya.publish.validate_unicode_strings.rst | 7 - ...lish.validate_unreal_mesh_triangulated.rst | 7 - ...lish.validate_unreal_staticmesh_naming.rst | 7 - ...s.maya.publish.validate_unreal_up_axis.rst | 7 - ...sh.validate_vray_distributed_rendering.rst | 7 - ....publish.validate_vray_referenced_aovs.rst | 7 - ...lish.validate_vray_translator_settings.rst | 7 - ...lugins.maya.publish.validate_vrayproxy.rst | 7 - ...aya.publish.validate_vrayproxy_members.rst | 7 - ...h.validate_yeti_renderscript_callbacks.rst | 7 - ....publish.validate_yeti_rig_cache_state.rst | 7 - ...sh.validate_yeti_rig_input_in_instance.rst | 7 - ...aya.publish.validate_yeti_rig_settings.rst | 7 - docs/source/pype.plugins.maya.rst | 15 -- docs/source/pype.plugins.rst | 15 -- docs/source/pype.pype_commands.rst | 7 - docs/source/pype.resources.rst | 7 - docs/source/pype.rst | 99 ------------ .../pype.scripts.export_maya_ass_job.rst | 7 - .../pype.scripts.fusion_switch_shot.rst | 7 - docs/source/pype.scripts.otio_burnin.rst | 7 - docs/source/pype.scripts.publish_deadline.rst | 7 - .../pype.scripts.publish_filesequence.rst | 7 - docs/source/pype.scripts.rst | 58 ------- docs/source/pype.scripts.slates.rst | 15 -- .../pype.scripts.slates.slate_base.api.rst | 7 - .../pype.scripts.slates.slate_base.base.rst | 7 - ...pype.scripts.slates.slate_base.example.rst | 7 - ...scripts.slates.slate_base.font_factory.rst | 7 - .../pype.scripts.slates.slate_base.items.rst | 7 - .../pype.scripts.slates.slate_base.layer.rst | 7 - .../pype.scripts.slates.slate_base.lib.rst | 7 - ...e.scripts.slates.slate_base.main_frame.rst | 7 - .../source/pype.scripts.slates.slate_base.rst | 74 --------- docs/source/pype.setdress_api.rst | 7 - docs/source/pype.settings.constants.rst | 7 - docs/source/pype.settings.handlers.rst | 7 - docs/source/pype.settings.lib.rst | 7 - docs/source/pype.settings.rst | 18 --- docs/source/pype.tests.lib.rst | 7 - docs/source/pype.tests.rst | 42 ----- .../pype.tests.test_avalon_plugin_presets.rst | 7 - ...ype.tests.test_lib_restructuralization.rst | 7 - .../pype.tests.test_mongo_performance.rst | 7 - .../source/pype.tests.test_pyblish_filter.rst | 7 - docs/source/pype.tools.assetcreator.app.rst | 7 - docs/source/pype.tools.assetcreator.model.rst | 7 - docs/source/pype.tools.assetcreator.rst | 34 ---- .../source/pype.tools.assetcreator.widget.rst | 7 - docs/source/pype.tools.launcher.actions.rst | 7 - docs/source/pype.tools.launcher.delegates.rst | 7 - .../source/pype.tools.launcher.flickcharm.rst | 7 - docs/source/pype.tools.launcher.lib.rst | 7 - docs/source/pype.tools.launcher.models.rst | 7 - docs/source/pype.tools.launcher.rst | 66 -------- docs/source/pype.tools.launcher.widgets.rst | 7 - docs/source/pype.tools.launcher.window.rst | 7 - docs/source/pype.tools.pyblish_pype.app.rst | 7 - .../pype.tools.pyblish_pype.awesome.rst | 7 - .../source/pype.tools.pyblish_pype.compat.rst | 7 - .../pype.tools.pyblish_pype.constants.rst | 7 - .../pype.tools.pyblish_pype.control.rst | 7 - .../pype.tools.pyblish_pype.delegate.rst | 7 - docs/source/pype.tools.pyblish_pype.mock.rst | 7 - docs/source/pype.tools.pyblish_pype.model.rst | 7 - docs/source/pype.tools.pyblish_pype.rst | 130 ---------------- .../pype.tools.pyblish_pype.settings.rst | 7 - docs/source/pype.tools.pyblish_pype.util.rst | 7 - ...yblish_pype.vendor.qtawesome.animation.rst | 7 - ...lish_pype.vendor.qtawesome.iconic_font.rst | 7 - ...pe.tools.pyblish_pype.vendor.qtawesome.rst | 26 ---- .../source/pype.tools.pyblish_pype.vendor.rst | 15 -- .../pype.tools.pyblish_pype.version.rst | 7 - docs/source/pype.tools.pyblish_pype.view.rst | 7 - .../pype.tools.pyblish_pype.widgets.rst | 7 - .../source/pype.tools.pyblish_pype.window.rst | 7 - docs/source/pype.tools.rst | 19 --- docs/source/pype.tools.settings.rst | 15 -- docs/source/pype.tools.settings.settings.rst | 16 -- .../pype.tools.settings.settings.style.rst | 7 - ...ettings.settings.widgets.anatomy_types.rst | 7 - ...e.tools.settings.settings.widgets.base.rst | 7 - ...s.settings.settings.widgets.item_types.rst | 7 - ...pe.tools.settings.settings.widgets.lib.rst | 7 - ...ttings.widgets.multiselection_combobox.rst | 7 - .../pype.tools.settings.settings.widgets.rst | 74 --------- ....tools.settings.settings.widgets.tests.rst | 7 - ...ools.settings.settings.widgets.widgets.rst | 7 - ...tools.settings.settings.widgets.window.rst | 7 - .../pype.tools.standalonepublish.app.rst | 7 - .../pype.tools.standalonepublish.publish.rst | 7 - docs/source/pype.tools.standalonepublish.rst | 34 ---- ....standalonepublish.widgets.model_asset.rst | 7 - ...widgets.model_filter_proxy_exact_match.rst | 7 - ...gets.model_filter_proxy_recursive_sort.rst | 7 - ...s.standalonepublish.widgets.model_node.rst | 7 - ...nepublish.widgets.model_tasks_template.rst | 7 - ...s.standalonepublish.widgets.model_tree.rst | 7 - ...h.widgets.model_tree_view_deselectable.rst | 7 - ...ls.standalonepublish.widgets.resources.rst | 7 - .../pype.tools.standalonepublish.widgets.rst | 146 ------------------ ...standalonepublish.widgets.widget_asset.rst | 7 - ...epublish.widgets.widget_component_item.rst | 7 - ...alonepublish.widgets.widget_components.rst | 7 - ...publish.widgets.widget_components_list.rst | 7 - ...alonepublish.widgets.widget_drop_empty.rst | 7 - ...alonepublish.widgets.widget_drop_frame.rst | 7 - ...tandalonepublish.widgets.widget_family.rst | 7 - ...lonepublish.widgets.widget_family_desc.rst | 7 - ...tandalonepublish.widgets.widget_shadow.rst | 7 - docs/source/pype.tools.tray.pype_tray.rst | 7 - docs/source/pype.tools.tray.rst | 15 -- docs/source/pype.tools.workfiles.app.rst | 7 - docs/source/pype.tools.workfiles.model.rst | 7 - docs/source/pype.tools.workfiles.rst | 17 -- docs/source/pype.tools.workfiles.view.rst | 7 - ....vendor.backports.configparser.helpers.rst | 7 - .../pype.vendor.backports.configparser.rst | 18 --- ...e.vendor.backports.functools_lru_cache.rst | 7 - docs/source/pype.vendor.backports.rst | 26 ---- docs/source/pype.vendor.builtins.rst | 7 - docs/source/pype.vendor.capture.rst | 7 - .../pype.vendor.capture_gui.accordion.rst | 7 - docs/source/pype.vendor.capture_gui.app.rst | 7 - .../pype.vendor.capture_gui.colorpicker.rst | 7 - docs/source/pype.vendor.capture_gui.lib.rst | 7 - .../source/pype.vendor.capture_gui.plugin.rst | 7 - .../pype.vendor.capture_gui.presets.rst | 7 - docs/source/pype.vendor.capture_gui.rst | 82 ---------- .../source/pype.vendor.capture_gui.tokens.rst | 7 - .../pype.vendor.capture_gui.vendor.Qt.rst | 7 - .../source/pype.vendor.capture_gui.vendor.rst | 18 --- .../pype.vendor.capture_gui.version.rst | 7 - ...pe.vendor.ftrack_api_old.accessor.base.rst | 7 - ...pe.vendor.ftrack_api_old.accessor.disk.rst | 7 - .../pype.vendor.ftrack_api_old.accessor.rst | 34 ---- ....vendor.ftrack_api_old.accessor.server.rst | 7 - .../pype.vendor.ftrack_api_old.attribute.rst | 7 - .../pype.vendor.ftrack_api_old.cache.rst | 7 - .../pype.vendor.ftrack_api_old.collection.rst | 7 - .../pype.vendor.ftrack_api_old.data.rst | 7 - ...or.ftrack_api_old.entity.asset_version.rst | 7 - ...pype.vendor.ftrack_api_old.entity.base.rst | 7 - ...vendor.ftrack_api_old.entity.component.rst | 7 - ...e.vendor.ftrack_api_old.entity.factory.rst | 7 - .../pype.vendor.ftrack_api_old.entity.job.rst | 7 - ....vendor.ftrack_api_old.entity.location.rst | 7 - ...pype.vendor.ftrack_api_old.entity.note.rst | 7 - ...r.ftrack_api_old.entity.project_schema.rst | 7 - .../pype.vendor.ftrack_api_old.entity.rst | 82 ---------- ...pype.vendor.ftrack_api_old.entity.user.rst | 7 - .../pype.vendor.ftrack_api_old.event.base.rst | 7 - ...vendor.ftrack_api_old.event.expression.rst | 7 - .../pype.vendor.ftrack_api_old.event.hub.rst | 7 - .../pype.vendor.ftrack_api_old.event.rst | 50 ------ ...vendor.ftrack_api_old.event.subscriber.rst | 7 - ...ndor.ftrack_api_old.event.subscription.rst | 7 - .../pype.vendor.ftrack_api_old.exception.rst | 7 - .../pype.vendor.ftrack_api_old.formatter.rst | 7 - .../pype.vendor.ftrack_api_old.inspection.rst | 7 - .../pype.vendor.ftrack_api_old.logging.rst | 7 - .../pype.vendor.ftrack_api_old.operation.rst | 7 - .../pype.vendor.ftrack_api_old.plugin.rst | 7 - .../pype.vendor.ftrack_api_old.query.rst | 7 - ...d.resource_identifier_transformer.base.rst | 7 - ...pi_old.resource_identifier_transformer.rst | 18 --- docs/source/pype.vendor.ftrack_api_old.rst | 126 --------------- .../pype.vendor.ftrack_api_old.session.rst | 7 - ...e.vendor.ftrack_api_old.structure.base.rst | 7 - ...dor.ftrack_api_old.structure.entity_id.rst | 7 - ...ype.vendor.ftrack_api_old.structure.id.rst | 7 - ...vendor.ftrack_api_old.structure.origin.rst | 7 - .../pype.vendor.ftrack_api_old.structure.rst | 50 ------ ...ndor.ftrack_api_old.structure.standard.rst | 7 - .../pype.vendor.ftrack_api_old.symbol.rst | 7 - docs/source/pype.vendor.pysync.rst | 7 - docs/source/pype.vendor.rst | 37 ----- docs/source/pype.version.rst | 7 - docs/source/pype.widgets.message_window.rst | 7 - docs/source/pype.widgets.popup.rst | 7 - docs/source/pype.widgets.project_settings.rst | 7 - docs/source/pype.widgets.rst | 34 ---- 484 files changed, 61 insertions(+), 5825 deletions(-) delete mode 100644 docs/source/igniter.bootstrap_repos.rst delete mode 100644 docs/source/igniter.install_dialog.rst delete mode 100644 docs/source/igniter.install_thread.rst delete mode 100644 docs/source/igniter.rst delete mode 100644 docs/source/igniter.tools.rst delete mode 100644 docs/source/pype.action.rst delete mode 100644 docs/source/pype.api.rst delete mode 100644 docs/source/pype.cli.rst delete mode 100644 docs/source/pype.hosts.aftereffects.rst delete mode 100644 docs/source/pype.hosts.blender.action.rst delete mode 100644 docs/source/pype.hosts.blender.plugin.rst delete mode 100644 docs/source/pype.hosts.blender.rst delete mode 100644 docs/source/pype.hosts.celaction.cli.rst delete mode 100644 docs/source/pype.hosts.celaction.rst delete mode 100644 docs/source/pype.hosts.fusion.lib.rst delete mode 100644 docs/source/pype.hosts.fusion.menu.rst delete mode 100644 docs/source/pype.hosts.fusion.pipeline.rst delete mode 100644 docs/source/pype.hosts.fusion.rst delete mode 100644 docs/source/pype.hosts.fusion.scripts.duplicate_with_inputs.rst delete mode 100644 docs/source/pype.hosts.fusion.scripts.fusion_switch_shot.rst delete mode 100644 docs/source/pype.hosts.fusion.scripts.rst delete mode 100644 docs/source/pype.hosts.fusion.scripts.set_rendermode.rst delete mode 100644 docs/source/pype.hosts.fusion.utils.rst delete mode 100644 docs/source/pype.hosts.harmony.rst delete mode 100644 docs/source/pype.hosts.hiero.events.rst delete mode 100644 docs/source/pype.hosts.hiero.lib.rst delete mode 100644 docs/source/pype.hosts.hiero.menu.rst delete mode 100644 docs/source/pype.hosts.hiero.rst delete mode 100644 docs/source/pype.hosts.hiero.tags.rst delete mode 100644 docs/source/pype.hosts.hiero.workio.rst delete mode 100644 docs/source/pype.hosts.houdini.lib.rst delete mode 100644 docs/source/pype.hosts.houdini.rst delete mode 100644 docs/source/pype.hosts.maya.action.rst delete mode 100644 docs/source/pype.hosts.maya.customize.rst delete mode 100644 docs/source/pype.hosts.maya.expected_files.rst delete mode 100644 docs/source/pype.hosts.maya.lib.rst delete mode 100644 docs/source/pype.hosts.maya.menu.rst delete mode 100644 docs/source/pype.hosts.maya.plugin.rst delete mode 100644 docs/source/pype.hosts.maya.rst delete mode 100644 docs/source/pype.hosts.nuke.actions.rst delete mode 100644 docs/source/pype.hosts.nuke.lib.rst delete mode 100644 docs/source/pype.hosts.nuke.menu.rst delete mode 100644 docs/source/pype.hosts.nuke.plugin.rst delete mode 100644 docs/source/pype.hosts.nuke.presets.rst delete mode 100644 docs/source/pype.hosts.nuke.rst delete mode 100644 docs/source/pype.hosts.nuke.utils.rst delete mode 100644 docs/source/pype.hosts.nukestudio.rst delete mode 100644 docs/source/pype.hosts.photoshop.rst delete mode 100644 docs/source/pype.hosts.premiere.lib.rst delete mode 100644 docs/source/pype.hosts.premiere.rst delete mode 100644 docs/source/pype.hosts.resolve.action.rst delete mode 100644 docs/source/pype.hosts.resolve.lib.rst delete mode 100644 docs/source/pype.hosts.resolve.menu.rst delete mode 100644 docs/source/pype.hosts.resolve.otio.davinci_export.rst delete mode 100644 docs/source/pype.hosts.resolve.otio.davinci_import.rst delete mode 100644 docs/source/pype.hosts.resolve.otio.rst delete mode 100644 docs/source/pype.hosts.resolve.otio.utils.rst delete mode 100644 docs/source/pype.hosts.resolve.pipeline.rst delete mode 100644 docs/source/pype.hosts.resolve.plugin.rst delete mode 100644 docs/source/pype.hosts.resolve.preload_console.rst delete mode 100644 docs/source/pype.hosts.resolve.rst delete mode 100644 docs/source/pype.hosts.resolve.todo-rendering.rst delete mode 100644 docs/source/pype.hosts.resolve.utils.rst delete mode 100644 docs/source/pype.hosts.resolve.workio.rst delete mode 100644 docs/source/pype.hosts.rst delete mode 100644 docs/source/pype.hosts.tvpaint.api.rst delete mode 100644 docs/source/pype.hosts.tvpaint.rst delete mode 100644 docs/source/pype.hosts.unreal.lib.rst delete mode 100644 docs/source/pype.hosts.unreal.plugin.rst delete mode 100644 docs/source/pype.hosts.unreal.rst delete mode 100644 docs/source/pype.launcher_actions.rst delete mode 100644 docs/source/pype.lib.abstract_collect_render.rst delete mode 100644 docs/source/pype.lib.abstract_expected_files.rst delete mode 100644 docs/source/pype.lib.abstract_metaplugins.rst delete mode 100644 docs/source/pype.lib.abstract_submit_deadline.rst delete mode 100644 docs/source/pype.lib.anatomy.rst delete mode 100644 docs/source/pype.lib.applications.rst delete mode 100644 docs/source/pype.lib.avalon_context.rst delete mode 100644 docs/source/pype.lib.config.rst delete mode 100644 docs/source/pype.lib.deprecated.rst delete mode 100644 docs/source/pype.lib.editorial.rst delete mode 100644 docs/source/pype.lib.env_tools.rst delete mode 100644 docs/source/pype.lib.execute.rst delete mode 100644 docs/source/pype.lib.ffmpeg_utils.rst delete mode 100644 docs/source/pype.lib.git_progress.rst delete mode 100644 docs/source/pype.lib.log.rst delete mode 100644 docs/source/pype.lib.mongo.rst delete mode 100644 docs/source/pype.lib.path_tools.rst delete mode 100644 docs/source/pype.lib.plugin_tools.rst delete mode 100644 docs/source/pype.lib.profiling.rst delete mode 100644 docs/source/pype.lib.python_module_tools.rst delete mode 100644 docs/source/pype.lib.rst delete mode 100644 docs/source/pype.lib.terminal.rst delete mode 100644 docs/source/pype.lib.terminal_splash.rst delete mode 100644 docs/source/pype.lib.user_settings.rst delete mode 100644 docs/source/pype.modules.adobe_communicator.adobe_comunicator.rst delete mode 100644 docs/source/pype.modules.adobe_communicator.lib.publish.rst delete mode 100644 docs/source/pype.modules.adobe_communicator.lib.rest_api.rst delete mode 100644 docs/source/pype.modules.adobe_communicator.lib.rst delete mode 100644 docs/source/pype.modules.adobe_communicator.rst delete mode 100644 docs/source/pype.modules.avalon_apps.avalon_app.rst delete mode 100644 docs/source/pype.modules.avalon_apps.rest_api.rst delete mode 100644 docs/source/pype.modules.avalon_apps.rst delete mode 100644 docs/source/pype.modules.base.rst delete mode 100644 docs/source/pype.modules.clockify.clockify.rst delete mode 100644 docs/source/pype.modules.clockify.clockify_api.rst delete mode 100644 docs/source/pype.modules.clockify.clockify_module.rst delete mode 100644 docs/source/pype.modules.clockify.constants.rst delete mode 100644 docs/source/pype.modules.clockify.rst delete mode 100644 docs/source/pype.modules.clockify.widgets.rst delete mode 100644 docs/source/pype.modules.deadline.deadline_module.rst delete mode 100644 docs/source/pype.modules.deadline.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_module.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.custom_db_connector.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.event_server_cli.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.ftrack_server.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.lib.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.socket_thread.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.sub_event_processor.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.sub_event_status.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.sub_event_storer.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.sub_legacy_server.rst delete mode 100644 docs/source/pype.modules.ftrack.ftrack_server.sub_user_server.rst delete mode 100644 docs/source/pype.modules.ftrack.lib.avalon_sync.rst delete mode 100644 docs/source/pype.modules.ftrack.lib.credentials.rst delete mode 100644 docs/source/pype.modules.ftrack.lib.ftrack_action_handler.rst delete mode 100644 docs/source/pype.modules.ftrack.lib.ftrack_app_handler.rst delete mode 100644 docs/source/pype.modules.ftrack.lib.ftrack_base_handler.rst delete mode 100644 docs/source/pype.modules.ftrack.lib.ftrack_event_handler.rst delete mode 100644 docs/source/pype.modules.ftrack.lib.rst delete mode 100644 docs/source/pype.modules.ftrack.lib.settings.rst delete mode 100644 docs/source/pype.modules.ftrack.rst delete mode 100644 docs/source/pype.modules.ftrack.tray.ftrack_module.rst delete mode 100644 docs/source/pype.modules.ftrack.tray.ftrack_tray.rst delete mode 100644 docs/source/pype.modules.ftrack.tray.login_dialog.rst delete mode 100644 docs/source/pype.modules.ftrack.tray.login_tools.rst delete mode 100644 docs/source/pype.modules.ftrack.tray.rst delete mode 100644 docs/source/pype.modules.idle_manager.idle_manager.rst delete mode 100644 docs/source/pype.modules.idle_manager.rst delete mode 100644 docs/source/pype.modules.launcher_action.rst delete mode 100644 docs/source/pype.modules.log_viewer.log_view_module.rst delete mode 100644 docs/source/pype.modules.log_viewer.rst delete mode 100644 docs/source/pype.modules.log_viewer.tray.app.rst delete mode 100644 docs/source/pype.modules.log_viewer.tray.models.rst delete mode 100644 docs/source/pype.modules.log_viewer.tray.rst delete mode 100644 docs/source/pype.modules.log_viewer.tray.widgets.rst delete mode 100644 docs/source/pype.modules.muster.muster.rst delete mode 100644 docs/source/pype.modules.muster.rst delete mode 100644 docs/source/pype.modules.muster.widget_login.rst delete mode 100644 docs/source/pype.modules.rest_api.base_class.rst delete mode 100644 docs/source/pype.modules.rest_api.lib.exceptions.rst delete mode 100644 docs/source/pype.modules.rest_api.lib.factory.rst delete mode 100644 docs/source/pype.modules.rest_api.lib.handler.rst delete mode 100644 docs/source/pype.modules.rest_api.lib.lib.rst delete mode 100644 docs/source/pype.modules.rest_api.lib.rst delete mode 100644 docs/source/pype.modules.rest_api.rest_api.rst delete mode 100644 docs/source/pype.modules.rest_api.rst delete mode 100644 docs/source/pype.modules.rst delete mode 100644 docs/source/pype.modules.settings_action.rst delete mode 100644 docs/source/pype.modules.standalonepublish.rst delete mode 100644 docs/source/pype.modules.standalonepublish.standalonepublish_module.rst delete mode 100644 docs/source/pype.modules.standalonepublish_action.rst delete mode 100644 docs/source/pype.modules.sync_server.rst delete mode 100644 docs/source/pype.modules.sync_server.sync_server.rst delete mode 100644 docs/source/pype.modules.sync_server.utils.rst delete mode 100644 docs/source/pype.modules.timers_manager.rst delete mode 100644 docs/source/pype.modules.timers_manager.timers_manager.rst delete mode 100644 docs/source/pype.modules.timers_manager.widget_user_idle.rst delete mode 100644 docs/source/pype.modules.user.rst delete mode 100644 docs/source/pype.modules.user.user_module.rst delete mode 100644 docs/source/pype.modules.user.widget_user.rst delete mode 100644 docs/source/pype.modules.websocket_server.hosts.aftereffects.rst delete mode 100644 docs/source/pype.modules.websocket_server.hosts.external_app_1.rst delete mode 100644 docs/source/pype.modules.websocket_server.hosts.photoshop.rst delete mode 100644 docs/source/pype.modules.websocket_server.hosts.rst delete mode 100644 docs/source/pype.modules.websocket_server.rst delete mode 100644 docs/source/pype.modules.websocket_server.websocket_server.rst delete mode 100644 docs/source/pype.modules_manager.rst delete mode 100644 docs/source/pype.plugin.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_animation.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_ass.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_assembly.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_file_dependencies.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_ftrack_family.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_history.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_instances.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_look.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_maya_units.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_maya_workspace.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_mayaascii.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_model.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_remove_marked.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_render.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_render_layer_aovs.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_renderable_camera.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_review.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_rig.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_scene.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_unreal_staticmesh.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_workscene_fps.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_yeti_cache.rst delete mode 100644 docs/source/pype.plugins.maya.publish.collect_yeti_rig.rst delete mode 100644 docs/source/pype.plugins.maya.publish.determine_future_version.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_animation.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_ass.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_assembly.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_assproxy.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_camera_alembic.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_camera_mayaScene.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_fbx.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_look.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_maya_scene_raw.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_model.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_playblast.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_pointcache.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_rendersetup.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_rig.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_thumbnail.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_vrayproxy.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_yeti_cache.rst delete mode 100644 docs/source/pype.plugins.maya.publish.extract_yeti_rig.rst delete mode 100644 docs/source/pype.plugins.maya.publish.increment_current_file_deadline.rst delete mode 100644 docs/source/pype.plugins.maya.publish.rst delete mode 100644 docs/source/pype.plugins.maya.publish.save_scene.rst delete mode 100644 docs/source/pype.plugins.maya.publish.submit_maya_deadline.rst delete mode 100644 docs/source/pype.plugins.maya.publish.submit_maya_muster.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_animation_content.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_animation_out_set_related_node_ids.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_ass_relative_paths.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_assembly_name.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_assembly_namespaces.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_assembly_transforms.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_attributes.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_camera_attributes.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_camera_contents.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_color_sets.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_current_renderlayer_renderable.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_deadline_connection.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_frame_range.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_instance_has_members.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_instance_subset.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_instancer_content.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_instancer_frame_ranges.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_joints_hidden.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_look_contents.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_look_default_shaders_connections.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_look_id_reference_edits.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_look_members_unique.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_look_no_default_shaders.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_look_sets.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_look_shading_group.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_look_single_shader.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_maya_units.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_arnold_attributes.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_has_uv.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_lamina_faces.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_no_negative_scale.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_non_manifold.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_non_zero_edge.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_normals_unlocked.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_overlapping_uvs.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_shader_connections.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_single_uv_set.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_uv_set_map1.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_mesh_vertices_have_edges.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_model_content.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_model_name.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_muster_connection.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_no_animation.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_no_default_camera.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_no_namespace.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_no_null_transforms.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_no_unknown_nodes.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_no_vraymesh.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_node_ids.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_node_ids_deformed_shapes.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_node_ids_in_database.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_node_ids_related.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_node_ids_unique.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_node_no_ghosting.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_render_image_rule.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_render_no_default_cameras.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_render_single_camera.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_renderlayer_aovs.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_rendersettings.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_resources.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_rig_contents.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_rig_controllers.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_rig_controllers_arnold_attributes.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_rig_out_set_node_ids.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_rig_output_ids.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_scene_set_workspace.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_shader_name.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_shape_default_names.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_shape_render_stats.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_single_assembly.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_skinCluster_deformer_set.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_step_size.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_transform_naming_suffix.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_transform_zero.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_unicode_strings.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_unreal_mesh_triangulated.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_unreal_staticmesh_naming.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_unreal_up_axis.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_vray_distributed_rendering.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_vray_referenced_aovs.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_vray_translator_settings.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_vrayproxy.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_vrayproxy_members.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_yeti_renderscript_callbacks.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_yeti_rig_cache_state.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_yeti_rig_input_in_instance.rst delete mode 100644 docs/source/pype.plugins.maya.publish.validate_yeti_rig_settings.rst delete mode 100644 docs/source/pype.plugins.maya.rst delete mode 100644 docs/source/pype.plugins.rst delete mode 100644 docs/source/pype.pype_commands.rst delete mode 100644 docs/source/pype.resources.rst delete mode 100644 docs/source/pype.rst delete mode 100644 docs/source/pype.scripts.export_maya_ass_job.rst delete mode 100644 docs/source/pype.scripts.fusion_switch_shot.rst delete mode 100644 docs/source/pype.scripts.otio_burnin.rst delete mode 100644 docs/source/pype.scripts.publish_deadline.rst delete mode 100644 docs/source/pype.scripts.publish_filesequence.rst delete mode 100644 docs/source/pype.scripts.rst delete mode 100644 docs/source/pype.scripts.slates.rst delete mode 100644 docs/source/pype.scripts.slates.slate_base.api.rst delete mode 100644 docs/source/pype.scripts.slates.slate_base.base.rst delete mode 100644 docs/source/pype.scripts.slates.slate_base.example.rst delete mode 100644 docs/source/pype.scripts.slates.slate_base.font_factory.rst delete mode 100644 docs/source/pype.scripts.slates.slate_base.items.rst delete mode 100644 docs/source/pype.scripts.slates.slate_base.layer.rst delete mode 100644 docs/source/pype.scripts.slates.slate_base.lib.rst delete mode 100644 docs/source/pype.scripts.slates.slate_base.main_frame.rst delete mode 100644 docs/source/pype.scripts.slates.slate_base.rst delete mode 100644 docs/source/pype.setdress_api.rst delete mode 100644 docs/source/pype.settings.constants.rst delete mode 100644 docs/source/pype.settings.handlers.rst delete mode 100644 docs/source/pype.settings.lib.rst delete mode 100644 docs/source/pype.settings.rst delete mode 100644 docs/source/pype.tests.lib.rst delete mode 100644 docs/source/pype.tests.rst delete mode 100644 docs/source/pype.tests.test_avalon_plugin_presets.rst delete mode 100644 docs/source/pype.tests.test_lib_restructuralization.rst delete mode 100644 docs/source/pype.tests.test_mongo_performance.rst delete mode 100644 docs/source/pype.tests.test_pyblish_filter.rst delete mode 100644 docs/source/pype.tools.assetcreator.app.rst delete mode 100644 docs/source/pype.tools.assetcreator.model.rst delete mode 100644 docs/source/pype.tools.assetcreator.rst delete mode 100644 docs/source/pype.tools.assetcreator.widget.rst delete mode 100644 docs/source/pype.tools.launcher.actions.rst delete mode 100644 docs/source/pype.tools.launcher.delegates.rst delete mode 100644 docs/source/pype.tools.launcher.flickcharm.rst delete mode 100644 docs/source/pype.tools.launcher.lib.rst delete mode 100644 docs/source/pype.tools.launcher.models.rst delete mode 100644 docs/source/pype.tools.launcher.rst delete mode 100644 docs/source/pype.tools.launcher.widgets.rst delete mode 100644 docs/source/pype.tools.launcher.window.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.app.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.awesome.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.compat.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.constants.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.control.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.delegate.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.mock.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.model.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.settings.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.util.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.vendor.qtawesome.animation.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.vendor.qtawesome.iconic_font.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.vendor.qtawesome.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.vendor.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.version.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.view.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.widgets.rst delete mode 100644 docs/source/pype.tools.pyblish_pype.window.rst delete mode 100644 docs/source/pype.tools.rst delete mode 100644 docs/source/pype.tools.settings.rst delete mode 100644 docs/source/pype.tools.settings.settings.rst delete mode 100644 docs/source/pype.tools.settings.settings.style.rst delete mode 100644 docs/source/pype.tools.settings.settings.widgets.anatomy_types.rst delete mode 100644 docs/source/pype.tools.settings.settings.widgets.base.rst delete mode 100644 docs/source/pype.tools.settings.settings.widgets.item_types.rst delete mode 100644 docs/source/pype.tools.settings.settings.widgets.lib.rst delete mode 100644 docs/source/pype.tools.settings.settings.widgets.multiselection_combobox.rst delete mode 100644 docs/source/pype.tools.settings.settings.widgets.rst delete mode 100644 docs/source/pype.tools.settings.settings.widgets.tests.rst delete mode 100644 docs/source/pype.tools.settings.settings.widgets.widgets.rst delete mode 100644 docs/source/pype.tools.settings.settings.widgets.window.rst delete mode 100644 docs/source/pype.tools.standalonepublish.app.rst delete mode 100644 docs/source/pype.tools.standalonepublish.publish.rst delete mode 100644 docs/source/pype.tools.standalonepublish.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.model_asset.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.model_filter_proxy_exact_match.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.model_filter_proxy_recursive_sort.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.model_node.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.model_tasks_template.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.model_tree.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.model_tree_view_deselectable.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.resources.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.widget_asset.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.widget_component_item.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.widget_components.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.widget_components_list.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.widget_drop_empty.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.widget_drop_frame.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.widget_family.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.widget_family_desc.rst delete mode 100644 docs/source/pype.tools.standalonepublish.widgets.widget_shadow.rst delete mode 100644 docs/source/pype.tools.tray.pype_tray.rst delete mode 100644 docs/source/pype.tools.tray.rst delete mode 100644 docs/source/pype.tools.workfiles.app.rst delete mode 100644 docs/source/pype.tools.workfiles.model.rst delete mode 100644 docs/source/pype.tools.workfiles.rst delete mode 100644 docs/source/pype.tools.workfiles.view.rst delete mode 100644 docs/source/pype.vendor.backports.configparser.helpers.rst delete mode 100644 docs/source/pype.vendor.backports.configparser.rst delete mode 100644 docs/source/pype.vendor.backports.functools_lru_cache.rst delete mode 100644 docs/source/pype.vendor.backports.rst delete mode 100644 docs/source/pype.vendor.builtins.rst delete mode 100644 docs/source/pype.vendor.capture.rst delete mode 100644 docs/source/pype.vendor.capture_gui.accordion.rst delete mode 100644 docs/source/pype.vendor.capture_gui.app.rst delete mode 100644 docs/source/pype.vendor.capture_gui.colorpicker.rst delete mode 100644 docs/source/pype.vendor.capture_gui.lib.rst delete mode 100644 docs/source/pype.vendor.capture_gui.plugin.rst delete mode 100644 docs/source/pype.vendor.capture_gui.presets.rst delete mode 100644 docs/source/pype.vendor.capture_gui.rst delete mode 100644 docs/source/pype.vendor.capture_gui.tokens.rst delete mode 100644 docs/source/pype.vendor.capture_gui.vendor.Qt.rst delete mode 100644 docs/source/pype.vendor.capture_gui.vendor.rst delete mode 100644 docs/source/pype.vendor.capture_gui.version.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.accessor.base.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.accessor.disk.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.accessor.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.accessor.server.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.attribute.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.cache.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.collection.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.data.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.asset_version.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.base.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.component.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.factory.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.job.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.location.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.note.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.project_schema.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.entity.user.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.event.base.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.event.expression.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.event.hub.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.event.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.event.subscriber.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.event.subscription.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.exception.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.formatter.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.inspection.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.logging.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.operation.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.plugin.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.query.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.resource_identifier_transformer.base.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.resource_identifier_transformer.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.session.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.structure.base.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.structure.entity_id.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.structure.id.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.structure.origin.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.structure.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.structure.standard.rst delete mode 100644 docs/source/pype.vendor.ftrack_api_old.symbol.rst delete mode 100644 docs/source/pype.vendor.pysync.rst delete mode 100644 docs/source/pype.vendor.rst delete mode 100644 docs/source/pype.version.rst delete mode 100644 docs/source/pype.widgets.message_window.rst delete mode 100644 docs/source/pype.widgets.popup.rst delete mode 100644 docs/source/pype.widgets.project_settings.rst delete mode 100644 docs/source/pype.widgets.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index 5b34ff8dc0..c54f51cbe9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,18 +17,29 @@ import os import sys -pype_root = os.path.abspath('../..') -sys.path.insert(0, pype_root) +from Qt.QtWidgets import QApplication + +openpype_root = os.path.abspath('../..') +sys.path.insert(0, openpype_root) +app = QApplication([]) + +""" repos = os.listdir(os.path.abspath("../../repos")) -repos = [os.path.join(pype_root, "repos", repo) for repo in repos] +repos = [os.path.join(openpype_root, "repos", repo) for repo in repos] for repo in repos: sys.path.append(repo) +""" + +todo_include_todos = True +autodoc_mock_imports = ["maya", "pymel", "nuke", "nukestudio", "nukescripts", + "hiero", "bpy", "fusion", "houdini", "hou", "unreal", + "__builtin__", "resolve", "pysync", "DaVinciResolveScript"] # -- Project information ----------------------------------------------------- -project = 'pype' -copyright = '2019, Orbi Tools' -author = 'Orbi Tools' +project = 'OpenPype' +copyright = '2023 Ynput' +author = 'Ynput' # The short X.Y version version = '' @@ -54,17 +65,46 @@ extensions = [ 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary', - 'recommonmark' + 'm2r2' + 'autoapi.extension' ] +############################## +# Autoapi settings +############################## + +autoapi_dirs = ['../../openpype', '../../igniter'] + +# bypas modules with a lot of python2 content for now +autoapi_ignore = [ + "*plugin*", + "*hosts*", + "*vendor*", + "*modules*", + "*setup*", + "*tools*", + "*schemas*", + "*website*" +] +autoapi_keep_files = True +autoapi_options = [ + 'members', + 'undoc-members', + 'show-inheritance', + 'show-module-summary' +] +autoapi_add_toctree_entry = True +autoapi_template_dir = '_autoapi_templates' + + # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ['templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ['.rst', '.md'] # The master toctree document. master_doc = 'index' @@ -79,7 +119,10 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] +exclude_patterns = [ + "openpype.hosts.resolve.*", + "openpype.tools.*" + ] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'friendly' @@ -104,7 +147,9 @@ html_theme = 'sphinx_rtd_theme' # documentation. # html_theme_options = { - 'collapse_navigation': False + 'collapse_navigation': False, + 'navigation_depth': 5, + 'titles_only': False } # Add any paths that contain custom static files (such as style sheets) here, @@ -153,8 +198,8 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'pype.tex', 'pype Documentation', - 'OrbiTools', 'manual'), + (master_doc, 'openpype.tex', 'OpenPype Documentation', + 'Ynput', 'manual'), ] @@ -163,7 +208,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'pype', 'pype Documentation', + (master_doc, 'openpype', 'OpenPype Documentation', [author], 1) ] @@ -174,8 +219,8 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'pype', 'pype Documentation', - author, 'pype', 'One line description of project.', + (master_doc, 'OpenPype', 'OpenPype Documentation', + author, 'OpenPype', 'Pipeline for studios', 'Miscellaneous'), ] diff --git a/docs/source/igniter.bootstrap_repos.rst b/docs/source/igniter.bootstrap_repos.rst deleted file mode 100644 index 7c6e0a0757..0000000000 --- a/docs/source/igniter.bootstrap_repos.rst +++ /dev/null @@ -1,7 +0,0 @@ -igniter.bootstrap\_repos module -=============================== - -.. automodule:: igniter.bootstrap_repos - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/igniter.install_dialog.rst b/docs/source/igniter.install_dialog.rst deleted file mode 100644 index bf30ec270e..0000000000 --- a/docs/source/igniter.install_dialog.rst +++ /dev/null @@ -1,7 +0,0 @@ -igniter.install\_dialog module -============================== - -.. automodule:: igniter.install_dialog - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/igniter.install_thread.rst b/docs/source/igniter.install_thread.rst deleted file mode 100644 index 6c19516219..0000000000 --- a/docs/source/igniter.install_thread.rst +++ /dev/null @@ -1,7 +0,0 @@ -igniter.install\_thread module -============================== - -.. automodule:: igniter.install_thread - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/igniter.rst b/docs/source/igniter.rst deleted file mode 100644 index b4aebe88b0..0000000000 --- a/docs/source/igniter.rst +++ /dev/null @@ -1,42 +0,0 @@ -igniter package -=============== - -.. automodule:: igniter - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -igniter.bootstrap\_repos module -------------------------------- - -.. automodule:: igniter.bootstrap_repos - :members: - :undoc-members: - :show-inheritance: - -igniter.install\_dialog module ------------------------------- - -.. automodule:: igniter.install_dialog - :members: - :undoc-members: - :show-inheritance: - -igniter.install\_thread module ------------------------------- - -.. automodule:: igniter.install_thread - :members: - :undoc-members: - :show-inheritance: - -igniter.tools module --------------------- - -.. automodule:: igniter.tools - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/igniter.tools.rst b/docs/source/igniter.tools.rst deleted file mode 100644 index 4fdbdf9d29..0000000000 --- a/docs/source/igniter.tools.rst +++ /dev/null @@ -1,7 +0,0 @@ -igniter.tools module -==================== - -.. automodule:: igniter.tools - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.action.rst b/docs/source/pype.action.rst deleted file mode 100644 index 62a32e08b5..0000000000 --- a/docs/source/pype.action.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.action module -================== - -.. automodule:: pype.action - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.api.rst b/docs/source/pype.api.rst deleted file mode 100644 index af3602a895..0000000000 --- a/docs/source/pype.api.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.api module -=============== - -.. automodule:: pype.api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.cli.rst b/docs/source/pype.cli.rst deleted file mode 100644 index 7e4a336fa9..0000000000 --- a/docs/source/pype.cli.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.cli module -=============== - -.. automodule:: pype.cli - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.aftereffects.rst b/docs/source/pype.hosts.aftereffects.rst deleted file mode 100644 index 3c2b2dda41..0000000000 --- a/docs/source/pype.hosts.aftereffects.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.aftereffects package -=============================== - -.. automodule:: pype.hosts.aftereffects - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.blender.action.rst b/docs/source/pype.hosts.blender.action.rst deleted file mode 100644 index a6444b1efc..0000000000 --- a/docs/source/pype.hosts.blender.action.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.blender.action module -================================ - -.. automodule:: pype.hosts.blender.action - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.blender.plugin.rst b/docs/source/pype.hosts.blender.plugin.rst deleted file mode 100644 index cf6a8feec8..0000000000 --- a/docs/source/pype.hosts.blender.plugin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.blender.plugin module -================================ - -.. automodule:: pype.hosts.blender.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.blender.rst b/docs/source/pype.hosts.blender.rst deleted file mode 100644 index 19cb85e5f3..0000000000 --- a/docs/source/pype.hosts.blender.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.hosts.blender package -========================== - -.. automodule:: pype.hosts.blender - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.blender.action module --------------------------------- - -.. automodule:: pype.hosts.blender.action - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.blender.plugin module --------------------------------- - -.. automodule:: pype.hosts.blender.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.celaction.cli.rst b/docs/source/pype.hosts.celaction.cli.rst deleted file mode 100644 index c8843b90bd..0000000000 --- a/docs/source/pype.hosts.celaction.cli.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.celaction.cli module -=============================== - -.. automodule:: pype.hosts.celaction.cli - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.celaction.rst b/docs/source/pype.hosts.celaction.rst deleted file mode 100644 index 1aa236397e..0000000000 --- a/docs/source/pype.hosts.celaction.rst +++ /dev/null @@ -1,18 +0,0 @@ -pype.hosts.celaction package -============================ - -.. automodule:: pype.hosts.celaction - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.celaction.cli module -------------------------------- - -.. automodule:: pype.hosts.celaction.cli - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.fusion.lib.rst b/docs/source/pype.hosts.fusion.lib.rst deleted file mode 100644 index 32b8f501f5..0000000000 --- a/docs/source/pype.hosts.fusion.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.fusion.lib module -============================ - -.. automodule:: pype.hosts.fusion.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.fusion.menu.rst b/docs/source/pype.hosts.fusion.menu.rst deleted file mode 100644 index ec5bf76612..0000000000 --- a/docs/source/pype.hosts.fusion.menu.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.fusion.menu module -============================= - -.. automodule:: pype.hosts.fusion.menu - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.fusion.pipeline.rst b/docs/source/pype.hosts.fusion.pipeline.rst deleted file mode 100644 index ff2a6440a8..0000000000 --- a/docs/source/pype.hosts.fusion.pipeline.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.fusion.pipeline module -================================= - -.. automodule:: pype.hosts.fusion.pipeline - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.fusion.rst b/docs/source/pype.hosts.fusion.rst deleted file mode 100644 index 7c2fee827c..0000000000 --- a/docs/source/pype.hosts.fusion.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.hosts.fusion package -========================= - -.. automodule:: pype.hosts.fusion - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.hosts.fusion.scripts - -Submodules ----------- - -pype.hosts.fusion.lib module ----------------------------- - -.. automodule:: pype.hosts.fusion.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.fusion.scripts.duplicate_with_inputs.rst b/docs/source/pype.hosts.fusion.scripts.duplicate_with_inputs.rst deleted file mode 100644 index 2503c20f3b..0000000000 --- a/docs/source/pype.hosts.fusion.scripts.duplicate_with_inputs.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.fusion.scripts.duplicate\_with\_inputs module -======================================================== - -.. automodule:: pype.hosts.fusion.scripts.duplicate_with_inputs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.fusion.scripts.fusion_switch_shot.rst b/docs/source/pype.hosts.fusion.scripts.fusion_switch_shot.rst deleted file mode 100644 index 770300116f..0000000000 --- a/docs/source/pype.hosts.fusion.scripts.fusion_switch_shot.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.fusion.scripts.fusion\_switch\_shot module -===================================================== - -.. automodule:: pype.hosts.fusion.scripts.fusion_switch_shot - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.fusion.scripts.rst b/docs/source/pype.hosts.fusion.scripts.rst deleted file mode 100644 index 5de5f66652..0000000000 --- a/docs/source/pype.hosts.fusion.scripts.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.hosts.fusion.scripts package -================================= - -.. automodule:: pype.hosts.fusion.scripts - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.fusion.scripts.fusion\_switch\_shot module ------------------------------------------------------ - -.. automodule:: pype.hosts.fusion.scripts.fusion_switch_shot - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.fusion.scripts.publish\_filesequence module ------------------------------------------------------- - -.. automodule:: pype.hosts.fusion.scripts.publish_filesequence - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.fusion.scripts.set_rendermode.rst b/docs/source/pype.hosts.fusion.scripts.set_rendermode.rst deleted file mode 100644 index 27bff63466..0000000000 --- a/docs/source/pype.hosts.fusion.scripts.set_rendermode.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.fusion.scripts.set\_rendermode module -================================================ - -.. automodule:: pype.hosts.fusion.scripts.set_rendermode - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.fusion.utils.rst b/docs/source/pype.hosts.fusion.utils.rst deleted file mode 100644 index b6de3d0510..0000000000 --- a/docs/source/pype.hosts.fusion.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.fusion.utils module -============================== - -.. automodule:: pype.hosts.fusion.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.harmony.rst b/docs/source/pype.hosts.harmony.rst deleted file mode 100644 index 60e1fcdce6..0000000000 --- a/docs/source/pype.hosts.harmony.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.harmony package -========================== - -.. automodule:: pype.hosts.harmony - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.hiero.events.rst b/docs/source/pype.hosts.hiero.events.rst deleted file mode 100644 index 874abbffba..0000000000 --- a/docs/source/pype.hosts.hiero.events.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.hiero.events module -============================== - -.. automodule:: pype.hosts.hiero.events - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.hiero.lib.rst b/docs/source/pype.hosts.hiero.lib.rst deleted file mode 100644 index 8c0d33b03b..0000000000 --- a/docs/source/pype.hosts.hiero.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.hiero.lib module -=========================== - -.. automodule:: pype.hosts.hiero.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.hiero.menu.rst b/docs/source/pype.hosts.hiero.menu.rst deleted file mode 100644 index baa1317e61..0000000000 --- a/docs/source/pype.hosts.hiero.menu.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.hiero.menu module -============================ - -.. automodule:: pype.hosts.hiero.menu - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.hiero.rst b/docs/source/pype.hosts.hiero.rst deleted file mode 100644 index 9a7891b45e..0000000000 --- a/docs/source/pype.hosts.hiero.rst +++ /dev/null @@ -1,19 +0,0 @@ -pype.hosts.hiero package -======================== - -.. automodule:: pype.hosts.hiero - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 10 - - pype.hosts.hiero.events - pype.hosts.hiero.lib - pype.hosts.hiero.menu - pype.hosts.hiero.tags - pype.hosts.hiero.workio diff --git a/docs/source/pype.hosts.hiero.tags.rst b/docs/source/pype.hosts.hiero.tags.rst deleted file mode 100644 index 0df33279d5..0000000000 --- a/docs/source/pype.hosts.hiero.tags.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.hiero.tags module -============================ - -.. automodule:: pype.hosts.hiero.tags - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.hiero.workio.rst b/docs/source/pype.hosts.hiero.workio.rst deleted file mode 100644 index 11aae43212..0000000000 --- a/docs/source/pype.hosts.hiero.workio.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.hiero.workio module -============================== - -.. automodule:: pype.hosts.hiero.workio - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.houdini.lib.rst b/docs/source/pype.hosts.houdini.lib.rst deleted file mode 100644 index ba6e60d5f3..0000000000 --- a/docs/source/pype.hosts.houdini.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.houdini.lib module -============================= - -.. automodule:: pype.hosts.houdini.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.houdini.rst b/docs/source/pype.hosts.houdini.rst deleted file mode 100644 index 5db18ab3d4..0000000000 --- a/docs/source/pype.hosts.houdini.rst +++ /dev/null @@ -1,18 +0,0 @@ -pype.hosts.houdini package -========================== - -.. automodule:: pype.hosts.houdini - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.houdini.lib module ------------------------------ - -.. automodule:: pype.hosts.houdini.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.maya.action.rst b/docs/source/pype.hosts.maya.action.rst deleted file mode 100644 index e1ad7e5d43..0000000000 --- a/docs/source/pype.hosts.maya.action.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.maya.action module -============================= - -.. automodule:: pype.hosts.maya.action - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.maya.customize.rst b/docs/source/pype.hosts.maya.customize.rst deleted file mode 100644 index 335e75b0d4..0000000000 --- a/docs/source/pype.hosts.maya.customize.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.maya.customize module -================================ - -.. automodule:: pype.hosts.maya.customize - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.maya.expected_files.rst b/docs/source/pype.hosts.maya.expected_files.rst deleted file mode 100644 index 0ecf22e502..0000000000 --- a/docs/source/pype.hosts.maya.expected_files.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.maya.expected\_files module -====================================== - -.. automodule:: pype.hosts.maya.expected_files - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.maya.lib.rst b/docs/source/pype.hosts.maya.lib.rst deleted file mode 100644 index 7d7dbe4502..0000000000 --- a/docs/source/pype.hosts.maya.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.maya.lib module -========================== - -.. automodule:: pype.hosts.maya.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.maya.menu.rst b/docs/source/pype.hosts.maya.menu.rst deleted file mode 100644 index 614e113769..0000000000 --- a/docs/source/pype.hosts.maya.menu.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.maya.menu module -=========================== - -.. automodule:: pype.hosts.maya.menu - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.maya.plugin.rst b/docs/source/pype.hosts.maya.plugin.rst deleted file mode 100644 index 5796b40c70..0000000000 --- a/docs/source/pype.hosts.maya.plugin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.maya.plugin module -============================= - -.. automodule:: pype.hosts.maya.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.maya.rst b/docs/source/pype.hosts.maya.rst deleted file mode 100644 index 0beab888fc..0000000000 --- a/docs/source/pype.hosts.maya.rst +++ /dev/null @@ -1,58 +0,0 @@ -pype.hosts.maya package -======================= - -.. automodule:: pype.hosts.maya - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.maya.action module ------------------------------ - -.. automodule:: pype.hosts.maya.action - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.maya.customize module --------------------------------- - -.. automodule:: pype.hosts.maya.customize - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.maya.expected\_files module --------------------------------------- - -.. automodule:: pype.hosts.maya.expected_files - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.maya.lib module --------------------------- - -.. automodule:: pype.hosts.maya.lib - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.maya.menu module ---------------------------- - -.. automodule:: pype.hosts.maya.menu - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.maya.plugin module ------------------------------ - -.. automodule:: pype.hosts.maya.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.nuke.actions.rst b/docs/source/pype.hosts.nuke.actions.rst deleted file mode 100644 index d5e8849a38..0000000000 --- a/docs/source/pype.hosts.nuke.actions.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.nuke.actions module -============================== - -.. automodule:: pype.hosts.nuke.actions - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.nuke.lib.rst b/docs/source/pype.hosts.nuke.lib.rst deleted file mode 100644 index c177a27f2d..0000000000 --- a/docs/source/pype.hosts.nuke.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.nuke.lib module -========================== - -.. automodule:: pype.hosts.nuke.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.nuke.menu.rst b/docs/source/pype.hosts.nuke.menu.rst deleted file mode 100644 index 190e488b95..0000000000 --- a/docs/source/pype.hosts.nuke.menu.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.nuke.menu module -=========================== - -.. automodule:: pype.hosts.nuke.menu - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.nuke.plugin.rst b/docs/source/pype.hosts.nuke.plugin.rst deleted file mode 100644 index ddd5f1db89..0000000000 --- a/docs/source/pype.hosts.nuke.plugin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.nuke.plugin module -============================= - -.. automodule:: pype.hosts.nuke.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.nuke.presets.rst b/docs/source/pype.hosts.nuke.presets.rst deleted file mode 100644 index a69aa8a367..0000000000 --- a/docs/source/pype.hosts.nuke.presets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.nuke.presets module -============================== - -.. automodule:: pype.hosts.nuke.presets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.nuke.rst b/docs/source/pype.hosts.nuke.rst deleted file mode 100644 index 559de65927..0000000000 --- a/docs/source/pype.hosts.nuke.rst +++ /dev/null @@ -1,58 +0,0 @@ -pype.hosts.nuke package -======================= - -.. automodule:: pype.hosts.nuke - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.nuke.actions module ------------------------------- - -.. automodule:: pype.hosts.nuke.actions - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.nuke.lib module --------------------------- - -.. automodule:: pype.hosts.nuke.lib - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.nuke.menu module ---------------------------- - -.. automodule:: pype.hosts.nuke.menu - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.nuke.plugin module ------------------------------ - -.. automodule:: pype.hosts.nuke.plugin - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.nuke.presets module ------------------------------- - -.. automodule:: pype.hosts.nuke.presets - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.nuke.utils module ----------------------------- - -.. automodule:: pype.hosts.nuke.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.nuke.utils.rst b/docs/source/pype.hosts.nuke.utils.rst deleted file mode 100644 index 66974dc707..0000000000 --- a/docs/source/pype.hosts.nuke.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.nuke.utils module -============================ - -.. automodule:: pype.hosts.nuke.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.nukestudio.rst b/docs/source/pype.hosts.nukestudio.rst deleted file mode 100644 index c718d699fa..0000000000 --- a/docs/source/pype.hosts.nukestudio.rst +++ /dev/null @@ -1,50 +0,0 @@ -pype.hosts.nukestudio package -============================= - -.. automodule:: pype.hosts.nukestudio - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.nukestudio.events module ------------------------------------ - -.. automodule:: pype.hosts.nukestudio.events - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.nukestudio.lib module --------------------------------- - -.. automodule:: pype.hosts.nukestudio.lib - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.nukestudio.menu module ---------------------------------- - -.. automodule:: pype.hosts.nukestudio.menu - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.nukestudio.tags module ---------------------------------- - -.. automodule:: pype.hosts.nukestudio.tags - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.nukestudio.workio module ------------------------------------ - -.. automodule:: pype.hosts.nukestudio.workio - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.photoshop.rst b/docs/source/pype.hosts.photoshop.rst deleted file mode 100644 index f77ea79874..0000000000 --- a/docs/source/pype.hosts.photoshop.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.photoshop package -============================ - -.. automodule:: pype.hosts.photoshop - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.premiere.lib.rst b/docs/source/pype.hosts.premiere.lib.rst deleted file mode 100644 index e2c2723841..0000000000 --- a/docs/source/pype.hosts.premiere.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.premiere.lib module -============================== - -.. automodule:: pype.hosts.premiere.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.premiere.rst b/docs/source/pype.hosts.premiere.rst deleted file mode 100644 index 7c38d52c22..0000000000 --- a/docs/source/pype.hosts.premiere.rst +++ /dev/null @@ -1,18 +0,0 @@ -pype.hosts.premiere package -=========================== - -.. automodule:: pype.hosts.premiere - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.premiere.lib module ------------------------------- - -.. automodule:: pype.hosts.premiere.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.action.rst b/docs/source/pype.hosts.resolve.action.rst deleted file mode 100644 index 781694781f..0000000000 --- a/docs/source/pype.hosts.resolve.action.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.action module -================================ - -.. automodule:: pype.hosts.resolve.action - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.lib.rst b/docs/source/pype.hosts.resolve.lib.rst deleted file mode 100644 index 5860f783cc..0000000000 --- a/docs/source/pype.hosts.resolve.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.lib module -============================= - -.. automodule:: pype.hosts.resolve.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.menu.rst b/docs/source/pype.hosts.resolve.menu.rst deleted file mode 100644 index df87dcde98..0000000000 --- a/docs/source/pype.hosts.resolve.menu.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.menu module -============================== - -.. automodule:: pype.hosts.resolve.menu - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.otio.davinci_export.rst b/docs/source/pype.hosts.resolve.otio.davinci_export.rst deleted file mode 100644 index 498f96a7ed..0000000000 --- a/docs/source/pype.hosts.resolve.otio.davinci_export.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.otio.davinci\_export module -============================================== - -.. automodule:: pype.hosts.resolve.otio.davinci_export - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.otio.davinci_import.rst b/docs/source/pype.hosts.resolve.otio.davinci_import.rst deleted file mode 100644 index 30f43cc9fe..0000000000 --- a/docs/source/pype.hosts.resolve.otio.davinci_import.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.otio.davinci\_import module -============================================== - -.. automodule:: pype.hosts.resolve.otio.davinci_import - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.otio.rst b/docs/source/pype.hosts.resolve.otio.rst deleted file mode 100644 index 523d8937ca..0000000000 --- a/docs/source/pype.hosts.resolve.otio.rst +++ /dev/null @@ -1,17 +0,0 @@ -pype.hosts.resolve.otio package -=============================== - -.. automodule:: pype.hosts.resolve.otio - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 10 - - pype.hosts.resolve.otio.davinci_export - pype.hosts.resolve.otio.davinci_import - pype.hosts.resolve.otio.utils diff --git a/docs/source/pype.hosts.resolve.otio.utils.rst b/docs/source/pype.hosts.resolve.otio.utils.rst deleted file mode 100644 index 765f492732..0000000000 --- a/docs/source/pype.hosts.resolve.otio.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.otio.utils module -==================================== - -.. automodule:: pype.hosts.resolve.otio.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.pipeline.rst b/docs/source/pype.hosts.resolve.pipeline.rst deleted file mode 100644 index 3efc24137b..0000000000 --- a/docs/source/pype.hosts.resolve.pipeline.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.pipeline module -================================== - -.. automodule:: pype.hosts.resolve.pipeline - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.plugin.rst b/docs/source/pype.hosts.resolve.plugin.rst deleted file mode 100644 index 26f6c56aef..0000000000 --- a/docs/source/pype.hosts.resolve.plugin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.plugin module -================================ - -.. automodule:: pype.hosts.resolve.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.preload_console.rst b/docs/source/pype.hosts.resolve.preload_console.rst deleted file mode 100644 index 0d38ae14ea..0000000000 --- a/docs/source/pype.hosts.resolve.preload_console.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.preload\_console module -========================================== - -.. automodule:: pype.hosts.resolve.preload_console - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.rst b/docs/source/pype.hosts.resolve.rst deleted file mode 100644 index 368129e43e..0000000000 --- a/docs/source/pype.hosts.resolve.rst +++ /dev/null @@ -1,74 +0,0 @@ -pype.hosts.resolve package -========================== - -.. automodule:: pype.hosts.resolve - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.resolve.action module --------------------------------- - -.. automodule:: pype.hosts.resolve.action - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.resolve.lib module ------------------------------ - -.. automodule:: pype.hosts.resolve.lib - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.resolve.menu module ------------------------------- - -.. automodule:: pype.hosts.resolve.menu - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.resolve.pipeline module ----------------------------------- - -.. automodule:: pype.hosts.resolve.pipeline - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.resolve.plugin module --------------------------------- - -.. automodule:: pype.hosts.resolve.plugin - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.resolve.preload\_console module ------------------------------------------- - -.. automodule:: pype.hosts.resolve.preload_console - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.resolve.utils module -------------------------------- - -.. automodule:: pype.hosts.resolve.utils - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.resolve.workio module --------------------------------- - -.. automodule:: pype.hosts.resolve.workio - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.todo-rendering.rst b/docs/source/pype.hosts.resolve.todo-rendering.rst deleted file mode 100644 index 8ea80183ce..0000000000 --- a/docs/source/pype.hosts.resolve.todo-rendering.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.todo\-rendering module -========================================= - -.. automodule:: pype.hosts.resolve.todo-rendering - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.utils.rst b/docs/source/pype.hosts.resolve.utils.rst deleted file mode 100644 index e390a5d026..0000000000 --- a/docs/source/pype.hosts.resolve.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.utils module -=============================== - -.. automodule:: pype.hosts.resolve.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.resolve.workio.rst b/docs/source/pype.hosts.resolve.workio.rst deleted file mode 100644 index 5dceb99d64..0000000000 --- a/docs/source/pype.hosts.resolve.workio.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.resolve.workio module -================================ - -.. automodule:: pype.hosts.resolve.workio - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.rst b/docs/source/pype.hosts.rst deleted file mode 100644 index e2d9121501..0000000000 --- a/docs/source/pype.hosts.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.hosts package -================== - -.. automodule:: pype.hosts - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.hosts.blender - pype.hosts.celaction - pype.hosts.fusion - pype.hosts.harmony - pype.hosts.houdini - pype.hosts.maya - pype.hosts.nuke - pype.hosts.nukestudio - pype.hosts.photoshop - pype.hosts.premiere - pype.hosts.resolve - pype.hosts.unreal diff --git a/docs/source/pype.hosts.tvpaint.api.rst b/docs/source/pype.hosts.tvpaint.api.rst deleted file mode 100644 index 43273e8ec5..0000000000 --- a/docs/source/pype.hosts.tvpaint.api.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.tvpaint.api package -============================== - -.. automodule:: pype.hosts.tvpaint.api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.tvpaint.rst b/docs/source/pype.hosts.tvpaint.rst deleted file mode 100644 index 561be3a9dc..0000000000 --- a/docs/source/pype.hosts.tvpaint.rst +++ /dev/null @@ -1,15 +0,0 @@ -pype.hosts.tvpaint package -========================== - -.. automodule:: pype.hosts.tvpaint - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 10 - - pype.hosts.tvpaint.api diff --git a/docs/source/pype.hosts.unreal.lib.rst b/docs/source/pype.hosts.unreal.lib.rst deleted file mode 100644 index b891e71c47..0000000000 --- a/docs/source/pype.hosts.unreal.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.unreal.lib module -============================ - -.. automodule:: pype.hosts.unreal.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.unreal.plugin.rst b/docs/source/pype.hosts.unreal.plugin.rst deleted file mode 100644 index e3ef81c7c7..0000000000 --- a/docs/source/pype.hosts.unreal.plugin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.hosts.unreal.plugin module -=============================== - -.. automodule:: pype.hosts.unreal.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.hosts.unreal.rst b/docs/source/pype.hosts.unreal.rst deleted file mode 100644 index f46140298b..0000000000 --- a/docs/source/pype.hosts.unreal.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.hosts.unreal package -========================= - -.. automodule:: pype.hosts.unreal - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.hosts.unreal.lib module ----------------------------- - -.. automodule:: pype.hosts.unreal.lib - :members: - :undoc-members: - :show-inheritance: - -pype.hosts.unreal.plugin module -------------------------------- - -.. automodule:: pype.hosts.unreal.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.launcher_actions.rst b/docs/source/pype.launcher_actions.rst deleted file mode 100644 index c7525acbd1..0000000000 --- a/docs/source/pype.launcher_actions.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.launcher\_actions module -============================= - -.. automodule:: pype.launcher_actions - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.abstract_collect_render.rst b/docs/source/pype.lib.abstract_collect_render.rst deleted file mode 100644 index d6adadc271..0000000000 --- a/docs/source/pype.lib.abstract_collect_render.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.abstract\_collect\_render module -========================================= - -.. automodule:: pype.lib.abstract_collect_render - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.abstract_expected_files.rst b/docs/source/pype.lib.abstract_expected_files.rst deleted file mode 100644 index 904aeb3375..0000000000 --- a/docs/source/pype.lib.abstract_expected_files.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.abstract\_expected\_files module -========================================= - -.. automodule:: pype.lib.abstract_expected_files - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.abstract_metaplugins.rst b/docs/source/pype.lib.abstract_metaplugins.rst deleted file mode 100644 index 9f2751b630..0000000000 --- a/docs/source/pype.lib.abstract_metaplugins.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.abstract\_metaplugins module -===================================== - -.. automodule:: pype.lib.abstract_metaplugins - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.abstract_submit_deadline.rst b/docs/source/pype.lib.abstract_submit_deadline.rst deleted file mode 100644 index a57222add3..0000000000 --- a/docs/source/pype.lib.abstract_submit_deadline.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.abstract\_submit\_deadline module -========================================== - -.. automodule:: pype.lib.abstract_submit_deadline - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.anatomy.rst b/docs/source/pype.lib.anatomy.rst deleted file mode 100644 index 7bddb37c8a..0000000000 --- a/docs/source/pype.lib.anatomy.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.anatomy module -======================= - -.. automodule:: pype.lib.anatomy - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.applications.rst b/docs/source/pype.lib.applications.rst deleted file mode 100644 index 8d1ff9b2c6..0000000000 --- a/docs/source/pype.lib.applications.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.applications module -============================ - -.. automodule:: pype.lib.applications - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.avalon_context.rst b/docs/source/pype.lib.avalon_context.rst deleted file mode 100644 index 067ea3380f..0000000000 --- a/docs/source/pype.lib.avalon_context.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.avalon\_context module -=============================== - -.. automodule:: pype.lib.avalon_context - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.config.rst b/docs/source/pype.lib.config.rst deleted file mode 100644 index ce4c13f4e7..0000000000 --- a/docs/source/pype.lib.config.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.config module -====================== - -.. automodule:: pype.lib.config - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.deprecated.rst b/docs/source/pype.lib.deprecated.rst deleted file mode 100644 index ec5ee58d67..0000000000 --- a/docs/source/pype.lib.deprecated.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.deprecated module -========================== - -.. automodule:: pype.lib.deprecated - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.editorial.rst b/docs/source/pype.lib.editorial.rst deleted file mode 100644 index d32e495e51..0000000000 --- a/docs/source/pype.lib.editorial.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.editorial module -========================= - -.. automodule:: pype.lib.editorial - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.env_tools.rst b/docs/source/pype.lib.env_tools.rst deleted file mode 100644 index cb470207c8..0000000000 --- a/docs/source/pype.lib.env_tools.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.env\_tools module -========================== - -.. automodule:: pype.lib.env_tools - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.execute.rst b/docs/source/pype.lib.execute.rst deleted file mode 100644 index 82c4ef0ad8..0000000000 --- a/docs/source/pype.lib.execute.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.execute module -======================= - -.. automodule:: pype.lib.execute - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.ffmpeg_utils.rst b/docs/source/pype.lib.ffmpeg_utils.rst deleted file mode 100644 index 968a3f39c8..0000000000 --- a/docs/source/pype.lib.ffmpeg_utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.ffmpeg\_utils module -============================= - -.. automodule:: pype.lib.ffmpeg_utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.git_progress.rst b/docs/source/pype.lib.git_progress.rst deleted file mode 100644 index 017cf4c3c7..0000000000 --- a/docs/source/pype.lib.git_progress.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.git\_progress module -============================= - -.. automodule:: pype.lib.git_progress - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.log.rst b/docs/source/pype.lib.log.rst deleted file mode 100644 index 6282178850..0000000000 --- a/docs/source/pype.lib.log.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.log module -=================== - -.. automodule:: pype.lib.log - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.mongo.rst b/docs/source/pype.lib.mongo.rst deleted file mode 100644 index 34fbc6af7f..0000000000 --- a/docs/source/pype.lib.mongo.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.mongo module -===================== - -.. automodule:: pype.lib.mongo - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.path_tools.rst b/docs/source/pype.lib.path_tools.rst deleted file mode 100644 index c19c41eea3..0000000000 --- a/docs/source/pype.lib.path_tools.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.path\_tools module -=========================== - -.. automodule:: pype.lib.path_tools - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.plugin_tools.rst b/docs/source/pype.lib.plugin_tools.rst deleted file mode 100644 index 6eadc5d3be..0000000000 --- a/docs/source/pype.lib.plugin_tools.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.plugin\_tools module -============================= - -.. automodule:: pype.lib.plugin_tools - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.profiling.rst b/docs/source/pype.lib.profiling.rst deleted file mode 100644 index 1fded0c8fd..0000000000 --- a/docs/source/pype.lib.profiling.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.profiling module -========================= - -.. automodule:: pype.lib.profiling - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.python_module_tools.rst b/docs/source/pype.lib.python_module_tools.rst deleted file mode 100644 index c916080bce..0000000000 --- a/docs/source/pype.lib.python_module_tools.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.python\_module\_tools module -===================================== - -.. automodule:: pype.lib.python_module_tools - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.rst b/docs/source/pype.lib.rst deleted file mode 100644 index ea880eea3e..0000000000 --- a/docs/source/pype.lib.rst +++ /dev/null @@ -1,90 +0,0 @@ -pype.lib package -================ - -.. automodule:: pype.lib - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.lib.anatomy module ------------------------ - -.. automodule:: pype.lib.anatomy - :members: - :undoc-members: - :show-inheritance: - -pype.lib.config module ----------------------- - -.. automodule:: pype.lib.config - :members: - :undoc-members: - :show-inheritance: - -pype.lib.execute module ------------------------ - -.. automodule:: pype.lib.execute - :members: - :undoc-members: - :show-inheritance: - -pype.lib.git\_progress module ------------------------------ - -.. automodule:: pype.lib.git_progress - :members: - :undoc-members: - :show-inheritance: - -pype.lib.lib module -------------------- - -.. automodule:: pype.lib.lib - :members: - :undoc-members: - :show-inheritance: - -pype.lib.log module -------------------- - -.. automodule:: pype.lib.log - :members: - :undoc-members: - :show-inheritance: - -pype.lib.mongo module ---------------------- - -.. automodule:: pype.lib.mongo - :members: - :undoc-members: - :show-inheritance: - -pype.lib.profiling module -------------------------- - -.. automodule:: pype.lib.profiling - :members: - :undoc-members: - :show-inheritance: - -pype.lib.terminal module ------------------------- - -.. automodule:: pype.lib.terminal - :members: - :undoc-members: - :show-inheritance: - -pype.lib.user\_settings module ------------------------------- - -.. automodule:: pype.lib.user_settings - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.terminal.rst b/docs/source/pype.lib.terminal.rst deleted file mode 100644 index dafe1d8f69..0000000000 --- a/docs/source/pype.lib.terminal.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.terminal module -======================== - -.. automodule:: pype.lib.terminal - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.terminal_splash.rst b/docs/source/pype.lib.terminal_splash.rst deleted file mode 100644 index 06038f0f09..0000000000 --- a/docs/source/pype.lib.terminal_splash.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.terminal\_splash module -================================ - -.. automodule:: pype.lib.terminal_splash - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.lib.user_settings.rst b/docs/source/pype.lib.user_settings.rst deleted file mode 100644 index 7b4e8ced78..0000000000 --- a/docs/source/pype.lib.user_settings.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.lib.user\_settings module -============================== - -.. automodule:: pype.lib.user_settings - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.adobe_communicator.adobe_comunicator.rst b/docs/source/pype.modules.adobe_communicator.adobe_comunicator.rst deleted file mode 100644 index aadbaa0dc5..0000000000 --- a/docs/source/pype.modules.adobe_communicator.adobe_comunicator.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.adobe\_communicator.adobe\_comunicator module -========================================================== - -.. automodule:: pype.modules.adobe_communicator.adobe_comunicator - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.adobe_communicator.lib.publish.rst b/docs/source/pype.modules.adobe_communicator.lib.publish.rst deleted file mode 100644 index a16bf1dd0a..0000000000 --- a/docs/source/pype.modules.adobe_communicator.lib.publish.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.adobe\_communicator.lib.publish module -=================================================== - -.. automodule:: pype.modules.adobe_communicator.lib.publish - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.adobe_communicator.lib.rest_api.rst b/docs/source/pype.modules.adobe_communicator.lib.rest_api.rst deleted file mode 100644 index 457bebef99..0000000000 --- a/docs/source/pype.modules.adobe_communicator.lib.rest_api.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.adobe\_communicator.lib.rest\_api module -===================================================== - -.. automodule:: pype.modules.adobe_communicator.lib.rest_api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.adobe_communicator.lib.rst b/docs/source/pype.modules.adobe_communicator.lib.rst deleted file mode 100644 index cdec4ce80e..0000000000 --- a/docs/source/pype.modules.adobe_communicator.lib.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.modules.adobe\_communicator.lib package -============================================ - -.. automodule:: pype.modules.adobe_communicator.lib - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.adobe\_communicator.lib.publish module ---------------------------------------------------- - -.. automodule:: pype.modules.adobe_communicator.lib.publish - :members: - :undoc-members: - :show-inheritance: - -pype.modules.adobe\_communicator.lib.rest\_api module ------------------------------------------------------ - -.. automodule:: pype.modules.adobe_communicator.lib.rest_api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.adobe_communicator.rst b/docs/source/pype.modules.adobe_communicator.rst deleted file mode 100644 index f2fa40ced4..0000000000 --- a/docs/source/pype.modules.adobe_communicator.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.modules.adobe\_communicator package -======================================== - -.. automodule:: pype.modules.adobe_communicator - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.modules.adobe_communicator.lib - -Submodules ----------- - -pype.modules.adobe\_communicator.adobe\_comunicator module ----------------------------------------------------------- - -.. automodule:: pype.modules.adobe_communicator.adobe_comunicator - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.avalon_apps.avalon_app.rst b/docs/source/pype.modules.avalon_apps.avalon_app.rst deleted file mode 100644 index 43f467e748..0000000000 --- a/docs/source/pype.modules.avalon_apps.avalon_app.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.avalon\_apps.avalon\_app module -============================================ - -.. automodule:: pype.modules.avalon_apps.avalon_app - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.avalon_apps.rest_api.rst b/docs/source/pype.modules.avalon_apps.rest_api.rst deleted file mode 100644 index d89c979311..0000000000 --- a/docs/source/pype.modules.avalon_apps.rest_api.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.avalon\_apps.rest\_api module -========================================== - -.. automodule:: pype.modules.avalon_apps.rest_api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.avalon_apps.rst b/docs/source/pype.modules.avalon_apps.rst deleted file mode 100644 index 4755eddae6..0000000000 --- a/docs/source/pype.modules.avalon_apps.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.modules.avalon\_apps package -================================= - -.. automodule:: pype.modules.avalon_apps - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.avalon\_apps.avalon\_app module --------------------------------------------- - -.. automodule:: pype.modules.avalon_apps.avalon_app - :members: - :undoc-members: - :show-inheritance: - -pype.modules.avalon\_apps.rest\_api module ------------------------------------------- - -.. automodule:: pype.modules.avalon_apps.rest_api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.base.rst b/docs/source/pype.modules.base.rst deleted file mode 100644 index 7cd3cfbd44..0000000000 --- a/docs/source/pype.modules.base.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.base module -======================== - -.. automodule:: pype.modules.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.clockify.clockify.rst b/docs/source/pype.modules.clockify.clockify.rst deleted file mode 100644 index a3deaab81d..0000000000 --- a/docs/source/pype.modules.clockify.clockify.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.clockify.clockify module -===================================== - -.. automodule:: pype.modules.clockify.clockify - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.clockify.clockify_api.rst b/docs/source/pype.modules.clockify.clockify_api.rst deleted file mode 100644 index 2facc550c5..0000000000 --- a/docs/source/pype.modules.clockify.clockify_api.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.clockify.clockify\_api module -========================================== - -.. automodule:: pype.modules.clockify.clockify_api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.clockify.clockify_module.rst b/docs/source/pype.modules.clockify.clockify_module.rst deleted file mode 100644 index 85f8e75ad1..0000000000 --- a/docs/source/pype.modules.clockify.clockify_module.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.clockify.clockify\_module module -============================================= - -.. automodule:: pype.modules.clockify.clockify_module - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.clockify.constants.rst b/docs/source/pype.modules.clockify.constants.rst deleted file mode 100644 index e30a073bfc..0000000000 --- a/docs/source/pype.modules.clockify.constants.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.clockify.constants module -====================================== - -.. automodule:: pype.modules.clockify.constants - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.clockify.rst b/docs/source/pype.modules.clockify.rst deleted file mode 100644 index 550ba049c2..0000000000 --- a/docs/source/pype.modules.clockify.rst +++ /dev/null @@ -1,42 +0,0 @@ -pype.modules.clockify package -============================= - -.. automodule:: pype.modules.clockify - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.clockify.clockify module -------------------------------------- - -.. automodule:: pype.modules.clockify.clockify - :members: - :undoc-members: - :show-inheritance: - -pype.modules.clockify.clockify\_api module ------------------------------------------- - -.. automodule:: pype.modules.clockify.clockify_api - :members: - :undoc-members: - :show-inheritance: - -pype.modules.clockify.constants module --------------------------------------- - -.. automodule:: pype.modules.clockify.constants - :members: - :undoc-members: - :show-inheritance: - -pype.modules.clockify.widgets module ------------------------------------- - -.. automodule:: pype.modules.clockify.widgets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.clockify.widgets.rst b/docs/source/pype.modules.clockify.widgets.rst deleted file mode 100644 index e9809fb048..0000000000 --- a/docs/source/pype.modules.clockify.widgets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.clockify.widgets module -==================================== - -.. automodule:: pype.modules.clockify.widgets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.deadline.deadline_module.rst b/docs/source/pype.modules.deadline.deadline_module.rst deleted file mode 100644 index 43e7198a8b..0000000000 --- a/docs/source/pype.modules.deadline.deadline_module.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.deadline.deadline\_module module -============================================= - -.. automodule:: pype.modules.deadline.deadline_module - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.deadline.rst b/docs/source/pype.modules.deadline.rst deleted file mode 100644 index 7633b2b950..0000000000 --- a/docs/source/pype.modules.deadline.rst +++ /dev/null @@ -1,15 +0,0 @@ -pype.modules.deadline package -============================= - -.. automodule:: pype.modules.deadline - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 10 - - pype.modules.deadline.deadline_module diff --git a/docs/source/pype.modules.ftrack.ftrack_module.rst b/docs/source/pype.modules.ftrack.ftrack_module.rst deleted file mode 100644 index 4188ffbed8..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_module.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_module module -========================================= - -.. automodule:: pype.modules.ftrack.ftrack_module - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.custom_db_connector.rst b/docs/source/pype.modules.ftrack.ftrack_server.custom_db_connector.rst deleted file mode 100644 index b42c3e054d..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.custom_db_connector.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.custom\_db\_connector module -=============================================================== - -.. automodule:: pype.modules.ftrack.ftrack_server.custom_db_connector - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.event_server_cli.rst b/docs/source/pype.modules.ftrack.ftrack_server.event_server_cli.rst deleted file mode 100644 index d6404f965c..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.event_server_cli.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.event\_server\_cli module -============================================================ - -.. automodule:: pype.modules.ftrack.ftrack_server.event_server_cli - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.ftrack_server.rst b/docs/source/pype.modules.ftrack.ftrack_server.ftrack_server.rst deleted file mode 100644 index af2783c263..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.ftrack_server.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.ftrack\_server module -======================================================== - -.. automodule:: pype.modules.ftrack.ftrack_server.ftrack_server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.lib.rst b/docs/source/pype.modules.ftrack.ftrack_server.lib.rst deleted file mode 100644 index 2ac4cef517..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.lib module -============================================= - -.. automodule:: pype.modules.ftrack.ftrack_server.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.rst b/docs/source/pype.modules.ftrack.ftrack_server.rst deleted file mode 100644 index 417acc1a45..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.rst +++ /dev/null @@ -1,90 +0,0 @@ -pype.modules.ftrack.ftrack\_server package -========================================== - -.. automodule:: pype.modules.ftrack.ftrack_server - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.ftrack.ftrack\_server.custom\_db\_connector module ---------------------------------------------------------------- - -.. automodule:: pype.modules.ftrack.ftrack_server.custom_db_connector - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.ftrack\_server.event\_server\_cli module ------------------------------------------------------------- - -.. automodule:: pype.modules.ftrack.ftrack_server.event_server_cli - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.ftrack\_server.ftrack\_server module --------------------------------------------------------- - -.. automodule:: pype.modules.ftrack.ftrack_server.ftrack_server - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.ftrack\_server.lib module ---------------------------------------------- - -.. automodule:: pype.modules.ftrack.ftrack_server.lib - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.ftrack\_server.socket\_thread module --------------------------------------------------------- - -.. automodule:: pype.modules.ftrack.ftrack_server.socket_thread - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.ftrack\_server.sub\_event\_processor module ---------------------------------------------------------------- - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_event_processor - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.ftrack\_server.sub\_event\_status module ------------------------------------------------------------- - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_event_status - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.ftrack\_server.sub\_event\_storer module ------------------------------------------------------------- - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_event_storer - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.ftrack\_server.sub\_legacy\_server module -------------------------------------------------------------- - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_legacy_server - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.ftrack\_server.sub\_user\_server module ------------------------------------------------------------ - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_user_server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.socket_thread.rst b/docs/source/pype.modules.ftrack.ftrack_server.socket_thread.rst deleted file mode 100644 index d8d24a8288..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.socket_thread.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.socket\_thread module -======================================================== - -.. automodule:: pype.modules.ftrack.ftrack_server.socket_thread - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.sub_event_processor.rst b/docs/source/pype.modules.ftrack.ftrack_server.sub_event_processor.rst deleted file mode 100644 index 04f863e347..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.sub_event_processor.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.sub\_event\_processor module -=============================================================== - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_event_processor - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.sub_event_status.rst b/docs/source/pype.modules.ftrack.ftrack_server.sub_event_status.rst deleted file mode 100644 index 876b7313cf..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.sub_event_status.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.sub\_event\_status module -============================================================ - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_event_status - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.sub_event_storer.rst b/docs/source/pype.modules.ftrack.ftrack_server.sub_event_storer.rst deleted file mode 100644 index 3d2d400d55..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.sub_event_storer.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.sub\_event\_storer module -============================================================ - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_event_storer - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.sub_legacy_server.rst b/docs/source/pype.modules.ftrack.ftrack_server.sub_legacy_server.rst deleted file mode 100644 index d25cdfe8de..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.sub_legacy_server.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.sub\_legacy\_server module -============================================================= - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_legacy_server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.ftrack_server.sub_user_server.rst b/docs/source/pype.modules.ftrack.ftrack_server.sub_user_server.rst deleted file mode 100644 index c13095d5f1..0000000000 --- a/docs/source/pype.modules.ftrack.ftrack_server.sub_user_server.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.ftrack\_server.sub\_user\_server module -=========================================================== - -.. automodule:: pype.modules.ftrack.ftrack_server.sub_user_server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.lib.avalon_sync.rst b/docs/source/pype.modules.ftrack.lib.avalon_sync.rst deleted file mode 100644 index 954ec4d911..0000000000 --- a/docs/source/pype.modules.ftrack.lib.avalon_sync.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.lib.avalon\_sync module -=========================================== - -.. automodule:: pype.modules.ftrack.lib.avalon_sync - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.lib.credentials.rst b/docs/source/pype.modules.ftrack.lib.credentials.rst deleted file mode 100644 index 3965dc406d..0000000000 --- a/docs/source/pype.modules.ftrack.lib.credentials.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.lib.credentials module -========================================== - -.. automodule:: pype.modules.ftrack.lib.credentials - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.lib.ftrack_action_handler.rst b/docs/source/pype.modules.ftrack.lib.ftrack_action_handler.rst deleted file mode 100644 index cec38f9b8a..0000000000 --- a/docs/source/pype.modules.ftrack.lib.ftrack_action_handler.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.lib.ftrack\_action\_handler module -====================================================== - -.. automodule:: pype.modules.ftrack.lib.ftrack_action_handler - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.lib.ftrack_app_handler.rst b/docs/source/pype.modules.ftrack.lib.ftrack_app_handler.rst deleted file mode 100644 index 1f7395927d..0000000000 --- a/docs/source/pype.modules.ftrack.lib.ftrack_app_handler.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.lib.ftrack\_app\_handler module -=================================================== - -.. automodule:: pype.modules.ftrack.lib.ftrack_app_handler - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.lib.ftrack_base_handler.rst b/docs/source/pype.modules.ftrack.lib.ftrack_base_handler.rst deleted file mode 100644 index 94fab7c940..0000000000 --- a/docs/source/pype.modules.ftrack.lib.ftrack_base_handler.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.lib.ftrack\_base\_handler module -==================================================== - -.. automodule:: pype.modules.ftrack.lib.ftrack_base_handler - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.lib.ftrack_event_handler.rst b/docs/source/pype.modules.ftrack.lib.ftrack_event_handler.rst deleted file mode 100644 index 0b57219b50..0000000000 --- a/docs/source/pype.modules.ftrack.lib.ftrack_event_handler.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.lib.ftrack\_event\_handler module -===================================================== - -.. automodule:: pype.modules.ftrack.lib.ftrack_event_handler - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.lib.rst b/docs/source/pype.modules.ftrack.lib.rst deleted file mode 100644 index 32a219ab3a..0000000000 --- a/docs/source/pype.modules.ftrack.lib.rst +++ /dev/null @@ -1,58 +0,0 @@ -pype.modules.ftrack.lib package -=============================== - -.. automodule:: pype.modules.ftrack.lib - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.ftrack.lib.avalon\_sync module -------------------------------------------- - -.. automodule:: pype.modules.ftrack.lib.avalon_sync - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.lib.credentials module ------------------------------------------- - -.. automodule:: pype.modules.ftrack.lib.credentials - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.lib.ftrack\_action\_handler module ------------------------------------------------------- - -.. automodule:: pype.modules.ftrack.lib.ftrack_action_handler - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.lib.ftrack\_app\_handler module ---------------------------------------------------- - -.. automodule:: pype.modules.ftrack.lib.ftrack_app_handler - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.lib.ftrack\_base\_handler module ----------------------------------------------------- - -.. automodule:: pype.modules.ftrack.lib.ftrack_base_handler - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.lib.ftrack\_event\_handler module ------------------------------------------------------ - -.. automodule:: pype.modules.ftrack.lib.ftrack_event_handler - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.lib.settings.rst b/docs/source/pype.modules.ftrack.lib.settings.rst deleted file mode 100644 index 255d52178a..0000000000 --- a/docs/source/pype.modules.ftrack.lib.settings.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.lib.settings module -======================================= - -.. automodule:: pype.modules.ftrack.lib.settings - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.rst b/docs/source/pype.modules.ftrack.rst deleted file mode 100644 index 13a92db808..0000000000 --- a/docs/source/pype.modules.ftrack.rst +++ /dev/null @@ -1,17 +0,0 @@ -pype.modules.ftrack package -=========================== - -.. automodule:: pype.modules.ftrack - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.modules.ftrack.ftrack_server - pype.modules.ftrack.lib - pype.modules.ftrack.tray diff --git a/docs/source/pype.modules.ftrack.tray.ftrack_module.rst b/docs/source/pype.modules.ftrack.tray.ftrack_module.rst deleted file mode 100644 index c4a370472c..0000000000 --- a/docs/source/pype.modules.ftrack.tray.ftrack_module.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.tray.ftrack\_module module -============================================== - -.. automodule:: pype.modules.ftrack.tray.ftrack_module - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.tray.ftrack_tray.rst b/docs/source/pype.modules.ftrack.tray.ftrack_tray.rst deleted file mode 100644 index 147647e9b4..0000000000 --- a/docs/source/pype.modules.ftrack.tray.ftrack_tray.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.tray.ftrack\_tray module -============================================ - -.. automodule:: pype.modules.ftrack.tray.ftrack_tray - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.tray.login_dialog.rst b/docs/source/pype.modules.ftrack.tray.login_dialog.rst deleted file mode 100644 index dabc2e73a7..0000000000 --- a/docs/source/pype.modules.ftrack.tray.login_dialog.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.tray.login\_dialog module -============================================= - -.. automodule:: pype.modules.ftrack.tray.login_dialog - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.tray.login_tools.rst b/docs/source/pype.modules.ftrack.tray.login_tools.rst deleted file mode 100644 index 00ec690866..0000000000 --- a/docs/source/pype.modules.ftrack.tray.login_tools.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.ftrack.tray.login\_tools module -============================================ - -.. automodule:: pype.modules.ftrack.tray.login_tools - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.ftrack.tray.rst b/docs/source/pype.modules.ftrack.tray.rst deleted file mode 100644 index 79772a9c3b..0000000000 --- a/docs/source/pype.modules.ftrack.tray.rst +++ /dev/null @@ -1,34 +0,0 @@ -pype.modules.ftrack.tray package -================================ - -.. automodule:: pype.modules.ftrack.tray - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.ftrack.tray.ftrack\_module module ----------------------------------------------- - -.. automodule:: pype.modules.ftrack.tray.ftrack_module - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.tray.login\_dialog module ---------------------------------------------- - -.. automodule:: pype.modules.ftrack.tray.login_dialog - :members: - :undoc-members: - :show-inheritance: - -pype.modules.ftrack.tray.login\_tools module --------------------------------------------- - -.. automodule:: pype.modules.ftrack.tray.login_tools - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.idle_manager.idle_manager.rst b/docs/source/pype.modules.idle_manager.idle_manager.rst deleted file mode 100644 index 8e93f97e6b..0000000000 --- a/docs/source/pype.modules.idle_manager.idle_manager.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.idle\_manager.idle\_manager module -=============================================== - -.. automodule:: pype.modules.idle_manager.idle_manager - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.idle_manager.rst b/docs/source/pype.modules.idle_manager.rst deleted file mode 100644 index a3f7922999..0000000000 --- a/docs/source/pype.modules.idle_manager.rst +++ /dev/null @@ -1,18 +0,0 @@ -pype.modules.idle\_manager package -================================== - -.. automodule:: pype.modules.idle_manager - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.idle\_manager.idle\_manager module ------------------------------------------------ - -.. automodule:: pype.modules.idle_manager.idle_manager - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.launcher_action.rst b/docs/source/pype.modules.launcher_action.rst deleted file mode 100644 index a63408e747..0000000000 --- a/docs/source/pype.modules.launcher_action.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.launcher\_action module -==================================== - -.. automodule:: pype.modules.launcher_action - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.log_viewer.log_view_module.rst b/docs/source/pype.modules.log_viewer.log_view_module.rst deleted file mode 100644 index 8d80170a9c..0000000000 --- a/docs/source/pype.modules.log_viewer.log_view_module.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.log\_viewer.log\_view\_module module -================================================= - -.. automodule:: pype.modules.log_viewer.log_view_module - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.log_viewer.rst b/docs/source/pype.modules.log_viewer.rst deleted file mode 100644 index e275d56086..0000000000 --- a/docs/source/pype.modules.log_viewer.rst +++ /dev/null @@ -1,23 +0,0 @@ -pype.modules.log\_viewer package -================================ - -.. automodule:: pype.modules.log_viewer - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 10 - - pype.modules.log_viewer.tray - -Submodules ----------- - -.. toctree:: - :maxdepth: 10 - - pype.modules.log_viewer.log_view_module diff --git a/docs/source/pype.modules.log_viewer.tray.app.rst b/docs/source/pype.modules.log_viewer.tray.app.rst deleted file mode 100644 index 0948a05594..0000000000 --- a/docs/source/pype.modules.log_viewer.tray.app.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.log\_viewer.tray.app module -======================================== - -.. automodule:: pype.modules.log_viewer.tray.app - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.log_viewer.tray.models.rst b/docs/source/pype.modules.log_viewer.tray.models.rst deleted file mode 100644 index 4da3887600..0000000000 --- a/docs/source/pype.modules.log_viewer.tray.models.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.log\_viewer.tray.models module -=========================================== - -.. automodule:: pype.modules.log_viewer.tray.models - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.log_viewer.tray.rst b/docs/source/pype.modules.log_viewer.tray.rst deleted file mode 100644 index 5f4b92f627..0000000000 --- a/docs/source/pype.modules.log_viewer.tray.rst +++ /dev/null @@ -1,17 +0,0 @@ -pype.modules.log\_viewer.tray package -===================================== - -.. automodule:: pype.modules.log_viewer.tray - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 10 - - pype.modules.log_viewer.tray.app - pype.modules.log_viewer.tray.models - pype.modules.log_viewer.tray.widgets diff --git a/docs/source/pype.modules.log_viewer.tray.widgets.rst b/docs/source/pype.modules.log_viewer.tray.widgets.rst deleted file mode 100644 index cb57c96559..0000000000 --- a/docs/source/pype.modules.log_viewer.tray.widgets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.log\_viewer.tray.widgets module -============================================ - -.. automodule:: pype.modules.log_viewer.tray.widgets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.muster.muster.rst b/docs/source/pype.modules.muster.muster.rst deleted file mode 100644 index d3ba1e7052..0000000000 --- a/docs/source/pype.modules.muster.muster.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.muster.muster module -================================= - -.. automodule:: pype.modules.muster.muster - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.muster.rst b/docs/source/pype.modules.muster.rst deleted file mode 100644 index d8d0f762f4..0000000000 --- a/docs/source/pype.modules.muster.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.modules.muster package -=========================== - -.. automodule:: pype.modules.muster - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.muster.muster module ---------------------------------- - -.. automodule:: pype.modules.muster.muster - :members: - :undoc-members: - :show-inheritance: - -pype.modules.muster.widget\_login module ----------------------------------------- - -.. automodule:: pype.modules.muster.widget_login - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.muster.widget_login.rst b/docs/source/pype.modules.muster.widget_login.rst deleted file mode 100644 index 1c59cec820..0000000000 --- a/docs/source/pype.modules.muster.widget_login.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.muster.widget\_login module -======================================== - -.. automodule:: pype.modules.muster.widget_login - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.rest_api.base_class.rst b/docs/source/pype.modules.rest_api.base_class.rst deleted file mode 100644 index c2a1030a78..0000000000 --- a/docs/source/pype.modules.rest_api.base_class.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.rest\_api.base\_class module -========================================= - -.. automodule:: pype.modules.rest_api.base_class - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.rest_api.lib.exceptions.rst b/docs/source/pype.modules.rest_api.lib.exceptions.rst deleted file mode 100644 index d755420ad0..0000000000 --- a/docs/source/pype.modules.rest_api.lib.exceptions.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.rest\_api.lib.exceptions module -============================================ - -.. automodule:: pype.modules.rest_api.lib.exceptions - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.rest_api.lib.factory.rst b/docs/source/pype.modules.rest_api.lib.factory.rst deleted file mode 100644 index 2131d1b8da..0000000000 --- a/docs/source/pype.modules.rest_api.lib.factory.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.rest\_api.lib.factory module -========================================= - -.. automodule:: pype.modules.rest_api.lib.factory - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.rest_api.lib.handler.rst b/docs/source/pype.modules.rest_api.lib.handler.rst deleted file mode 100644 index 6e340daf9b..0000000000 --- a/docs/source/pype.modules.rest_api.lib.handler.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.rest\_api.lib.handler module -========================================= - -.. automodule:: pype.modules.rest_api.lib.handler - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.rest_api.lib.lib.rst b/docs/source/pype.modules.rest_api.lib.lib.rst deleted file mode 100644 index 19663788e0..0000000000 --- a/docs/source/pype.modules.rest_api.lib.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.rest\_api.lib.lib module -===================================== - -.. automodule:: pype.modules.rest_api.lib.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.rest_api.lib.rst b/docs/source/pype.modules.rest_api.lib.rst deleted file mode 100644 index ed8288ee73..0000000000 --- a/docs/source/pype.modules.rest_api.lib.rst +++ /dev/null @@ -1,42 +0,0 @@ -pype.modules.rest\_api.lib package -================================== - -.. automodule:: pype.modules.rest_api.lib - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.rest\_api.lib.exceptions module --------------------------------------------- - -.. automodule:: pype.modules.rest_api.lib.exceptions - :members: - :undoc-members: - :show-inheritance: - -pype.modules.rest\_api.lib.factory module ------------------------------------------ - -.. automodule:: pype.modules.rest_api.lib.factory - :members: - :undoc-members: - :show-inheritance: - -pype.modules.rest\_api.lib.handler module ------------------------------------------ - -.. automodule:: pype.modules.rest_api.lib.handler - :members: - :undoc-members: - :show-inheritance: - -pype.modules.rest\_api.lib.lib module -------------------------------------- - -.. automodule:: pype.modules.rest_api.lib.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.rest_api.rest_api.rst b/docs/source/pype.modules.rest_api.rest_api.rst deleted file mode 100644 index e3d951ac9f..0000000000 --- a/docs/source/pype.modules.rest_api.rest_api.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.rest\_api.rest\_api module -======================================= - -.. automodule:: pype.modules.rest_api.rest_api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.rest_api.rst b/docs/source/pype.modules.rest_api.rst deleted file mode 100644 index 09c58c84f8..0000000000 --- a/docs/source/pype.modules.rest_api.rst +++ /dev/null @@ -1,34 +0,0 @@ -pype.modules.rest\_api package -============================== - -.. automodule:: pype.modules.rest_api - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.modules.rest_api.lib - -Submodules ----------- - -pype.modules.rest\_api.base\_class module ------------------------------------------ - -.. automodule:: pype.modules.rest_api.base_class - :members: - :undoc-members: - :show-inheritance: - -pype.modules.rest\_api.rest\_api module ---------------------------------------- - -.. automodule:: pype.modules.rest_api.rest_api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.rst b/docs/source/pype.modules.rst deleted file mode 100644 index 148c2084b4..0000000000 --- a/docs/source/pype.modules.rst +++ /dev/null @@ -1,36 +0,0 @@ -pype.modules package -==================== - -.. automodule:: pype.modules - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.modules.adobe_communicator - pype.modules.avalon_apps - pype.modules.clockify - pype.modules.ftrack - pype.modules.idle_manager - pype.modules.muster - pype.modules.rest_api - pype.modules.standalonepublish - pype.modules.timers_manager - pype.modules.user - pype.modules.websocket_server - -Submodules ----------- - -pype.modules.base module ------------------------- - -.. automodule:: pype.modules.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.settings_action.rst b/docs/source/pype.modules.settings_action.rst deleted file mode 100644 index 10f0881ced..0000000000 --- a/docs/source/pype.modules.settings_action.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.settings\_action module -==================================== - -.. automodule:: pype.modules.settings_action - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.standalonepublish.rst b/docs/source/pype.modules.standalonepublish.rst deleted file mode 100644 index 2ed366af5c..0000000000 --- a/docs/source/pype.modules.standalonepublish.rst +++ /dev/null @@ -1,18 +0,0 @@ -pype.modules.standalonepublish package -====================================== - -.. automodule:: pype.modules.standalonepublish - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.standalonepublish.standalonepublish\_module module ---------------------------------------------------------------- - -.. automodule:: pype.modules.standalonepublish.standalonepublish_module - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.standalonepublish.standalonepublish_module.rst b/docs/source/pype.modules.standalonepublish.standalonepublish_module.rst deleted file mode 100644 index a78826a4b4..0000000000 --- a/docs/source/pype.modules.standalonepublish.standalonepublish_module.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.standalonepublish.standalonepublish\_module module -=============================================================== - -.. automodule:: pype.modules.standalonepublish.standalonepublish_module - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.standalonepublish_action.rst b/docs/source/pype.modules.standalonepublish_action.rst deleted file mode 100644 index d51dbcefa0..0000000000 --- a/docs/source/pype.modules.standalonepublish_action.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.standalonepublish\_action module -============================================= - -.. automodule:: pype.modules.standalonepublish_action - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.sync_server.rst b/docs/source/pype.modules.sync_server.rst deleted file mode 100644 index a26dc7e212..0000000000 --- a/docs/source/pype.modules.sync_server.rst +++ /dev/null @@ -1,16 +0,0 @@ -pype.modules.sync\_server package -================================= - -.. automodule:: pype.modules.sync_server - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 10 - - pype.modules.sync_server.sync_server - pype.modules.sync_server.utils diff --git a/docs/source/pype.modules.sync_server.sync_server.rst b/docs/source/pype.modules.sync_server.sync_server.rst deleted file mode 100644 index 36d6aa68ed..0000000000 --- a/docs/source/pype.modules.sync_server.sync_server.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.sync\_server.sync\_server module -============================================= - -.. automodule:: pype.modules.sync_server.sync_server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.sync_server.utils.rst b/docs/source/pype.modules.sync_server.utils.rst deleted file mode 100644 index 325d5e435d..0000000000 --- a/docs/source/pype.modules.sync_server.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.sync\_server.utils module -====================================== - -.. automodule:: pype.modules.sync_server.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.timers_manager.rst b/docs/source/pype.modules.timers_manager.rst deleted file mode 100644 index 6c971e9dc1..0000000000 --- a/docs/source/pype.modules.timers_manager.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.modules.timers\_manager package -==================================== - -.. automodule:: pype.modules.timers_manager - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.timers\_manager.timers\_manager module ---------------------------------------------------- - -.. automodule:: pype.modules.timers_manager.timers_manager - :members: - :undoc-members: - :show-inheritance: - -pype.modules.timers\_manager.widget\_user\_idle module ------------------------------------------------------- - -.. automodule:: pype.modules.timers_manager.widget_user_idle - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.timers_manager.timers_manager.rst b/docs/source/pype.modules.timers_manager.timers_manager.rst deleted file mode 100644 index fe18e4d15c..0000000000 --- a/docs/source/pype.modules.timers_manager.timers_manager.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.timers\_manager.timers\_manager module -=================================================== - -.. automodule:: pype.modules.timers_manager.timers_manager - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.timers_manager.widget_user_idle.rst b/docs/source/pype.modules.timers_manager.widget_user_idle.rst deleted file mode 100644 index b072879c7a..0000000000 --- a/docs/source/pype.modules.timers_manager.widget_user_idle.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.timers\_manager.widget\_user\_idle module -====================================================== - -.. automodule:: pype.modules.timers_manager.widget_user_idle - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.user.rst b/docs/source/pype.modules.user.rst deleted file mode 100644 index d181b263e5..0000000000 --- a/docs/source/pype.modules.user.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.modules.user package -========================= - -.. automodule:: pype.modules.user - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.user.user\_module module -------------------------------------- - -.. automodule:: pype.modules.user.user_module - :members: - :undoc-members: - :show-inheritance: - -pype.modules.user.widget\_user module -------------------------------------- - -.. automodule:: pype.modules.user.widget_user - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.user.user_module.rst b/docs/source/pype.modules.user.user_module.rst deleted file mode 100644 index a8e0cd6bad..0000000000 --- a/docs/source/pype.modules.user.user_module.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.user.user\_module module -===================================== - -.. automodule:: pype.modules.user.user_module - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.user.widget_user.rst b/docs/source/pype.modules.user.widget_user.rst deleted file mode 100644 index 2979e5ead4..0000000000 --- a/docs/source/pype.modules.user.widget_user.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.user.widget\_user module -===================================== - -.. automodule:: pype.modules.user.widget_user - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.websocket_server.hosts.aftereffects.rst b/docs/source/pype.modules.websocket_server.hosts.aftereffects.rst deleted file mode 100644 index 9f4720ae14..0000000000 --- a/docs/source/pype.modules.websocket_server.hosts.aftereffects.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.websocket\_server.hosts.aftereffects module -======================================================== - -.. automodule:: pype.modules.websocket_server.hosts.aftereffects - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.websocket_server.hosts.external_app_1.rst b/docs/source/pype.modules.websocket_server.hosts.external_app_1.rst deleted file mode 100644 index 4ac69d9015..0000000000 --- a/docs/source/pype.modules.websocket_server.hosts.external_app_1.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.websocket\_server.hosts.external\_app\_1 module -============================================================ - -.. automodule:: pype.modules.websocket_server.hosts.external_app_1 - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.websocket_server.hosts.photoshop.rst b/docs/source/pype.modules.websocket_server.hosts.photoshop.rst deleted file mode 100644 index cbda61275a..0000000000 --- a/docs/source/pype.modules.websocket_server.hosts.photoshop.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.websocket\_server.hosts.photoshop module -===================================================== - -.. automodule:: pype.modules.websocket_server.hosts.photoshop - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.websocket_server.hosts.rst b/docs/source/pype.modules.websocket_server.hosts.rst deleted file mode 100644 index d5ce7c3f8e..0000000000 --- a/docs/source/pype.modules.websocket_server.hosts.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.modules.websocket\_server.hosts package -============================================ - -.. automodule:: pype.modules.websocket_server.hosts - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.modules.websocket\_server.hosts.external\_app\_1 module ------------------------------------------------------------- - -.. automodule:: pype.modules.websocket_server.hosts.external_app_1 - :members: - :undoc-members: - :show-inheritance: - -pype.modules.websocket\_server.hosts.photoshop module ------------------------------------------------------ - -.. automodule:: pype.modules.websocket_server.hosts.photoshop - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.websocket_server.rst b/docs/source/pype.modules.websocket_server.rst deleted file mode 100644 index a83d371df1..0000000000 --- a/docs/source/pype.modules.websocket_server.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.modules.websocket\_server package -====================================== - -.. automodule:: pype.modules.websocket_server - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.modules.websocket_server.hosts - -Submodules ----------- - -pype.modules.websocket\_server.websocket\_server module -------------------------------------------------------- - -.. automodule:: pype.modules.websocket_server.websocket_server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules.websocket_server.websocket_server.rst b/docs/source/pype.modules.websocket_server.websocket_server.rst deleted file mode 100644 index 354c9e6cf9..0000000000 --- a/docs/source/pype.modules.websocket_server.websocket_server.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules.websocket\_server.websocket\_server module -======================================================= - -.. automodule:: pype.modules.websocket_server.websocket_server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.modules_manager.rst b/docs/source/pype.modules_manager.rst deleted file mode 100644 index a5f2327d65..0000000000 --- a/docs/source/pype.modules_manager.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.modules\_manager module -============================ - -.. automodule:: pype.modules_manager - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugin.rst b/docs/source/pype.plugin.rst deleted file mode 100644 index c20bb77b2b..0000000000 --- a/docs/source/pype.plugin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugin module -================== - -.. automodule:: pype.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_animation.rst b/docs/source/pype.plugins.maya.publish.collect_animation.rst deleted file mode 100644 index 497c497057..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_animation.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_animation module -=================================================== - -.. automodule:: pype.plugins.maya.publish.collect_animation - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_ass.rst b/docs/source/pype.plugins.maya.publish.collect_ass.rst deleted file mode 100644 index a44e61ce98..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_ass.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_ass module -============================================= - -.. automodule:: pype.plugins.maya.publish.collect_ass - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_assembly.rst b/docs/source/pype.plugins.maya.publish.collect_assembly.rst deleted file mode 100644 index 5baa91818b..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_assembly.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_assembly module -================================================== - -.. automodule:: pype.plugins.maya.publish.collect_assembly - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_file_dependencies.rst b/docs/source/pype.plugins.maya.publish.collect_file_dependencies.rst deleted file mode 100644 index efe857140e..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_file_dependencies.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_file\_dependencies module -============================================================ - -.. automodule:: pype.plugins.maya.publish.collect_file_dependencies - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_ftrack_family.rst b/docs/source/pype.plugins.maya.publish.collect_ftrack_family.rst deleted file mode 100644 index 872bbc69a4..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_ftrack_family.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_ftrack\_family module -======================================================== - -.. automodule:: pype.plugins.maya.publish.collect_ftrack_family - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_history.rst b/docs/source/pype.plugins.maya.publish.collect_history.rst deleted file mode 100644 index 5a98778c24..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_history.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_history module -================================================= - -.. automodule:: pype.plugins.maya.publish.collect_history - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_instances.rst b/docs/source/pype.plugins.maya.publish.collect_instances.rst deleted file mode 100644 index 33c8b97597..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_instances.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_instances module -=================================================== - -.. automodule:: pype.plugins.maya.publish.collect_instances - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_look.rst b/docs/source/pype.plugins.maya.publish.collect_look.rst deleted file mode 100644 index 234fcf20d1..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_look.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_look module -============================================== - -.. automodule:: pype.plugins.maya.publish.collect_look - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_maya_units.rst b/docs/source/pype.plugins.maya.publish.collect_maya_units.rst deleted file mode 100644 index 0cb01b0fa7..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_maya_units.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_maya\_units module -===================================================== - -.. automodule:: pype.plugins.maya.publish.collect_maya_units - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_maya_workspace.rst b/docs/source/pype.plugins.maya.publish.collect_maya_workspace.rst deleted file mode 100644 index 7447052004..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_maya_workspace.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_maya\_workspace module -========================================================= - -.. automodule:: pype.plugins.maya.publish.collect_maya_workspace - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_mayaascii.rst b/docs/source/pype.plugins.maya.publish.collect_mayaascii.rst deleted file mode 100644 index 14fe826229..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_mayaascii.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_mayaascii module -=================================================== - -.. automodule:: pype.plugins.maya.publish.collect_mayaascii - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_model.rst b/docs/source/pype.plugins.maya.publish.collect_model.rst deleted file mode 100644 index b30bf3fb22..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_model.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_model module -=============================================== - -.. automodule:: pype.plugins.maya.publish.collect_model - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_remove_marked.rst b/docs/source/pype.plugins.maya.publish.collect_remove_marked.rst deleted file mode 100644 index a0bf9498d7..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_remove_marked.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_remove\_marked module -======================================================== - -.. automodule:: pype.plugins.maya.publish.collect_remove_marked - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_render.rst b/docs/source/pype.plugins.maya.publish.collect_render.rst deleted file mode 100644 index 6de8827119..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_render.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_render module -================================================ - -.. automodule:: pype.plugins.maya.publish.collect_render - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_render_layer_aovs.rst b/docs/source/pype.plugins.maya.publish.collect_render_layer_aovs.rst deleted file mode 100644 index ab511fc5dd..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_render_layer_aovs.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_render\_layer\_aovs module -============================================================= - -.. automodule:: pype.plugins.maya.publish.collect_render_layer_aovs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_renderable_camera.rst b/docs/source/pype.plugins.maya.publish.collect_renderable_camera.rst deleted file mode 100644 index c98e8000a1..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_renderable_camera.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_renderable\_camera module -============================================================ - -.. automodule:: pype.plugins.maya.publish.collect_renderable_camera - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_review.rst b/docs/source/pype.plugins.maya.publish.collect_review.rst deleted file mode 100644 index d73127aa85..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_review.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_review module -================================================ - -.. automodule:: pype.plugins.maya.publish.collect_review - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_rig.rst b/docs/source/pype.plugins.maya.publish.collect_rig.rst deleted file mode 100644 index e7c0528482..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_rig.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_rig module -============================================= - -.. automodule:: pype.plugins.maya.publish.collect_rig - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_scene.rst b/docs/source/pype.plugins.maya.publish.collect_scene.rst deleted file mode 100644 index c5c2fef222..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_scene.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_scene module -=============================================== - -.. automodule:: pype.plugins.maya.publish.collect_scene - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_unreal_staticmesh.rst b/docs/source/pype.plugins.maya.publish.collect_unreal_staticmesh.rst deleted file mode 100644 index 673f0865fd..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_unreal_staticmesh.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_unreal\_staticmesh module -============================================================ - -.. automodule:: pype.plugins.maya.publish.collect_unreal_staticmesh - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_workscene_fps.rst b/docs/source/pype.plugins.maya.publish.collect_workscene_fps.rst deleted file mode 100644 index ed4386a7ba..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_workscene_fps.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_workscene\_fps module -======================================================== - -.. automodule:: pype.plugins.maya.publish.collect_workscene_fps - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_yeti_cache.rst b/docs/source/pype.plugins.maya.publish.collect_yeti_cache.rst deleted file mode 100644 index 32ab50baca..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_yeti_cache.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_yeti\_cache module -===================================================== - -.. automodule:: pype.plugins.maya.publish.collect_yeti_cache - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.collect_yeti_rig.rst b/docs/source/pype.plugins.maya.publish.collect_yeti_rig.rst deleted file mode 100644 index 8cf968b7c5..0000000000 --- a/docs/source/pype.plugins.maya.publish.collect_yeti_rig.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.collect\_yeti\_rig module -=================================================== - -.. automodule:: pype.plugins.maya.publish.collect_yeti_rig - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.determine_future_version.rst b/docs/source/pype.plugins.maya.publish.determine_future_version.rst deleted file mode 100644 index 55c6155680..0000000000 --- a/docs/source/pype.plugins.maya.publish.determine_future_version.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.determine\_future\_version module -=========================================================== - -.. automodule:: pype.plugins.maya.publish.determine_future_version - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_animation.rst b/docs/source/pype.plugins.maya.publish.extract_animation.rst deleted file mode 100644 index 3649723042..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_animation.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_animation module -=================================================== - -.. automodule:: pype.plugins.maya.publish.extract_animation - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_ass.rst b/docs/source/pype.plugins.maya.publish.extract_ass.rst deleted file mode 100644 index be8123e5d7..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_ass.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_ass module -============================================= - -.. automodule:: pype.plugins.maya.publish.extract_ass - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_assembly.rst b/docs/source/pype.plugins.maya.publish.extract_assembly.rst deleted file mode 100644 index b36e8f6d30..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_assembly.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_assembly module -================================================== - -.. automodule:: pype.plugins.maya.publish.extract_assembly - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_assproxy.rst b/docs/source/pype.plugins.maya.publish.extract_assproxy.rst deleted file mode 100644 index fc97a2ee46..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_assproxy.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_assproxy module -================================================== - -.. automodule:: pype.plugins.maya.publish.extract_assproxy - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_camera_alembic.rst b/docs/source/pype.plugins.maya.publish.extract_camera_alembic.rst deleted file mode 100644 index a9df3da011..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_camera_alembic.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_camera\_alembic module -========================================================= - -.. automodule:: pype.plugins.maya.publish.extract_camera_alembic - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_camera_mayaScene.rst b/docs/source/pype.plugins.maya.publish.extract_camera_mayaScene.rst deleted file mode 100644 index db1799f52f..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_camera_mayaScene.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_camera\_mayaScene module -=========================================================== - -.. automodule:: pype.plugins.maya.publish.extract_camera_mayaScene - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_fbx.rst b/docs/source/pype.plugins.maya.publish.extract_fbx.rst deleted file mode 100644 index fffd5a6394..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_fbx.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_fbx module -============================================= - -.. automodule:: pype.plugins.maya.publish.extract_fbx - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_look.rst b/docs/source/pype.plugins.maya.publish.extract_look.rst deleted file mode 100644 index f2708678ce..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_look.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_look module -============================================== - -.. automodule:: pype.plugins.maya.publish.extract_look - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_maya_scene_raw.rst b/docs/source/pype.plugins.maya.publish.extract_maya_scene_raw.rst deleted file mode 100644 index 1e080dd0eb..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_maya_scene_raw.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_maya\_scene\_raw module -========================================================== - -.. automodule:: pype.plugins.maya.publish.extract_maya_scene_raw - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_model.rst b/docs/source/pype.plugins.maya.publish.extract_model.rst deleted file mode 100644 index c78b49c777..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_model.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_model module -=============================================== - -.. automodule:: pype.plugins.maya.publish.extract_model - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_playblast.rst b/docs/source/pype.plugins.maya.publish.extract_playblast.rst deleted file mode 100644 index 1aa284b370..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_playblast.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_playblast module -=================================================== - -.. automodule:: pype.plugins.maya.publish.extract_playblast - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_pointcache.rst b/docs/source/pype.plugins.maya.publish.extract_pointcache.rst deleted file mode 100644 index 97ebde4933..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_pointcache.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_pointcache module -==================================================== - -.. automodule:: pype.plugins.maya.publish.extract_pointcache - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_rendersetup.rst b/docs/source/pype.plugins.maya.publish.extract_rendersetup.rst deleted file mode 100644 index 86cb178f42..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_rendersetup.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_rendersetup module -===================================================== - -.. automodule:: pype.plugins.maya.publish.extract_rendersetup - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_rig.rst b/docs/source/pype.plugins.maya.publish.extract_rig.rst deleted file mode 100644 index f6419c9473..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_rig.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_rig module -============================================= - -.. automodule:: pype.plugins.maya.publish.extract_rig - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_thumbnail.rst b/docs/source/pype.plugins.maya.publish.extract_thumbnail.rst deleted file mode 100644 index 2d03e11d55..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_thumbnail.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_thumbnail module -=================================================== - -.. automodule:: pype.plugins.maya.publish.extract_thumbnail - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_vrayproxy.rst b/docs/source/pype.plugins.maya.publish.extract_vrayproxy.rst deleted file mode 100644 index 5439ff59ca..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_vrayproxy.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_vrayproxy module -=================================================== - -.. automodule:: pype.plugins.maya.publish.extract_vrayproxy - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_yeti_cache.rst b/docs/source/pype.plugins.maya.publish.extract_yeti_cache.rst deleted file mode 100644 index 7ad84dfc70..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_yeti_cache.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_yeti\_cache module -===================================================== - -.. automodule:: pype.plugins.maya.publish.extract_yeti_cache - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.extract_yeti_rig.rst b/docs/source/pype.plugins.maya.publish.extract_yeti_rig.rst deleted file mode 100644 index 76d483d91b..0000000000 --- a/docs/source/pype.plugins.maya.publish.extract_yeti_rig.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.extract\_yeti\_rig module -=================================================== - -.. automodule:: pype.plugins.maya.publish.extract_yeti_rig - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.increment_current_file_deadline.rst b/docs/source/pype.plugins.maya.publish.increment_current_file_deadline.rst deleted file mode 100644 index 97126a6c77..0000000000 --- a/docs/source/pype.plugins.maya.publish.increment_current_file_deadline.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.increment\_current\_file\_deadline module -=================================================================== - -.. automodule:: pype.plugins.maya.publish.increment_current_file_deadline - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.rst b/docs/source/pype.plugins.maya.publish.rst deleted file mode 100644 index dba0a9118c..0000000000 --- a/docs/source/pype.plugins.maya.publish.rst +++ /dev/null @@ -1,146 +0,0 @@ -pype.plugins.maya.publish package -================================= - -.. automodule:: pype.plugins.maya.publish - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 10 - - pype.plugins.maya.publish.collect_animation - pype.plugins.maya.publish.collect_ass - pype.plugins.maya.publish.collect_assembly - pype.plugins.maya.publish.collect_file_dependencies - pype.plugins.maya.publish.collect_ftrack_family - pype.plugins.maya.publish.collect_history - pype.plugins.maya.publish.collect_instances - pype.plugins.maya.publish.collect_look - pype.plugins.maya.publish.collect_maya_units - pype.plugins.maya.publish.collect_maya_workspace - pype.plugins.maya.publish.collect_mayaascii - pype.plugins.maya.publish.collect_model - pype.plugins.maya.publish.collect_remove_marked - pype.plugins.maya.publish.collect_render - pype.plugins.maya.publish.collect_render_layer_aovs - pype.plugins.maya.publish.collect_renderable_camera - pype.plugins.maya.publish.collect_review - pype.plugins.maya.publish.collect_rig - pype.plugins.maya.publish.collect_scene - pype.plugins.maya.publish.collect_unreal_staticmesh - pype.plugins.maya.publish.collect_workscene_fps - pype.plugins.maya.publish.collect_yeti_cache - pype.plugins.maya.publish.collect_yeti_rig - pype.plugins.maya.publish.determine_future_version - pype.plugins.maya.publish.extract_animation - pype.plugins.maya.publish.extract_ass - pype.plugins.maya.publish.extract_assembly - pype.plugins.maya.publish.extract_assproxy - pype.plugins.maya.publish.extract_camera_alembic - pype.plugins.maya.publish.extract_camera_mayaScene - pype.plugins.maya.publish.extract_fbx - pype.plugins.maya.publish.extract_look - pype.plugins.maya.publish.extract_maya_scene_raw - pype.plugins.maya.publish.extract_model - pype.plugins.maya.publish.extract_playblast - pype.plugins.maya.publish.extract_pointcache - pype.plugins.maya.publish.extract_rendersetup - pype.plugins.maya.publish.extract_rig - pype.plugins.maya.publish.extract_thumbnail - pype.plugins.maya.publish.extract_vrayproxy - pype.plugins.maya.publish.extract_yeti_cache - pype.plugins.maya.publish.extract_yeti_rig - pype.plugins.maya.publish.increment_current_file_deadline - pype.plugins.maya.publish.save_scene - pype.plugins.maya.publish.submit_maya_deadline - pype.plugins.maya.publish.submit_maya_muster - pype.plugins.maya.publish.validate_animation_content - pype.plugins.maya.publish.validate_animation_out_set_related_node_ids - pype.plugins.maya.publish.validate_ass_relative_paths - pype.plugins.maya.publish.validate_assembly_name - pype.plugins.maya.publish.validate_assembly_namespaces - pype.plugins.maya.publish.validate_assembly_transforms - pype.plugins.maya.publish.validate_attributes - pype.plugins.maya.publish.validate_camera_attributes - pype.plugins.maya.publish.validate_camera_contents - pype.plugins.maya.publish.validate_color_sets - pype.plugins.maya.publish.validate_current_renderlayer_renderable - pype.plugins.maya.publish.validate_deadline_connection - pype.plugins.maya.publish.validate_frame_range - pype.plugins.maya.publish.validate_instance_has_members - pype.plugins.maya.publish.validate_instance_subset - pype.plugins.maya.publish.validate_instancer_content - pype.plugins.maya.publish.validate_instancer_frame_ranges - pype.plugins.maya.publish.validate_joints_hidden - pype.plugins.maya.publish.validate_look_contents - pype.plugins.maya.publish.validate_look_default_shaders_connections - pype.plugins.maya.publish.validate_look_id_reference_edits - pype.plugins.maya.publish.validate_look_members_unique - pype.plugins.maya.publish.validate_look_no_default_shaders - pype.plugins.maya.publish.validate_look_sets - pype.plugins.maya.publish.validate_look_shading_group - pype.plugins.maya.publish.validate_look_single_shader - pype.plugins.maya.publish.validate_maya_units - pype.plugins.maya.publish.validate_mesh_arnold_attributes - pype.plugins.maya.publish.validate_mesh_has_uv - pype.plugins.maya.publish.validate_mesh_lamina_faces - pype.plugins.maya.publish.validate_mesh_no_negative_scale - pype.plugins.maya.publish.validate_mesh_non_manifold - pype.plugins.maya.publish.validate_mesh_non_zero_edge - pype.plugins.maya.publish.validate_mesh_normals_unlocked - pype.plugins.maya.publish.validate_mesh_overlapping_uvs - pype.plugins.maya.publish.validate_mesh_shader_connections - pype.plugins.maya.publish.validate_mesh_single_uv_set - pype.plugins.maya.publish.validate_mesh_uv_set_map1 - pype.plugins.maya.publish.validate_mesh_vertices_have_edges - pype.plugins.maya.publish.validate_model_content - pype.plugins.maya.publish.validate_model_name - pype.plugins.maya.publish.validate_muster_connection - pype.plugins.maya.publish.validate_no_animation - pype.plugins.maya.publish.validate_no_default_camera - pype.plugins.maya.publish.validate_no_namespace - pype.plugins.maya.publish.validate_no_null_transforms - pype.plugins.maya.publish.validate_no_unknown_nodes - pype.plugins.maya.publish.validate_no_vraymesh - pype.plugins.maya.publish.validate_node_ids - pype.plugins.maya.publish.validate_node_ids_deformed_shapes - pype.plugins.maya.publish.validate_node_ids_in_database - pype.plugins.maya.publish.validate_node_ids_related - pype.plugins.maya.publish.validate_node_ids_unique - pype.plugins.maya.publish.validate_node_no_ghosting - pype.plugins.maya.publish.validate_render_image_rule - pype.plugins.maya.publish.validate_render_no_default_cameras - pype.plugins.maya.publish.validate_render_single_camera - pype.plugins.maya.publish.validate_renderlayer_aovs - pype.plugins.maya.publish.validate_rendersettings - pype.plugins.maya.publish.validate_resources - pype.plugins.maya.publish.validate_rig_contents - pype.plugins.maya.publish.validate_rig_controllers - pype.plugins.maya.publish.validate_rig_controllers_arnold_attributes - pype.plugins.maya.publish.validate_rig_out_set_node_ids - pype.plugins.maya.publish.validate_rig_output_ids - pype.plugins.maya.publish.validate_scene_set_workspace - pype.plugins.maya.publish.validate_shader_name - pype.plugins.maya.publish.validate_shape_default_names - pype.plugins.maya.publish.validate_shape_render_stats - pype.plugins.maya.publish.validate_single_assembly - pype.plugins.maya.publish.validate_skinCluster_deformer_set - pype.plugins.maya.publish.validate_step_size - pype.plugins.maya.publish.validate_transform_naming_suffix - pype.plugins.maya.publish.validate_transform_zero - pype.plugins.maya.publish.validate_unicode_strings - pype.plugins.maya.publish.validate_unreal_mesh_triangulated - pype.plugins.maya.publish.validate_unreal_staticmesh_naming - pype.plugins.maya.publish.validate_unreal_up_axis - pype.plugins.maya.publish.validate_vray_distributed_rendering - pype.plugins.maya.publish.validate_vray_translator_settings - pype.plugins.maya.publish.validate_vrayproxy - pype.plugins.maya.publish.validate_vrayproxy_members - pype.plugins.maya.publish.validate_yeti_renderscript_callbacks - pype.plugins.maya.publish.validate_yeti_rig_cache_state - pype.plugins.maya.publish.validate_yeti_rig_input_in_instance - pype.plugins.maya.publish.validate_yeti_rig_settings diff --git a/docs/source/pype.plugins.maya.publish.save_scene.rst b/docs/source/pype.plugins.maya.publish.save_scene.rst deleted file mode 100644 index 2537bca03d..0000000000 --- a/docs/source/pype.plugins.maya.publish.save_scene.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.save\_scene module -============================================ - -.. automodule:: pype.plugins.maya.publish.save_scene - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.submit_maya_deadline.rst b/docs/source/pype.plugins.maya.publish.submit_maya_deadline.rst deleted file mode 100644 index 0e521cec4e..0000000000 --- a/docs/source/pype.plugins.maya.publish.submit_maya_deadline.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.submit\_maya\_deadline module -======================================================= - -.. automodule:: pype.plugins.maya.publish.submit_maya_deadline - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.submit_maya_muster.rst b/docs/source/pype.plugins.maya.publish.submit_maya_muster.rst deleted file mode 100644 index 4ae263e157..0000000000 --- a/docs/source/pype.plugins.maya.publish.submit_maya_muster.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.submit\_maya\_muster module -===================================================== - -.. automodule:: pype.plugins.maya.publish.submit_maya_muster - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_animation_content.rst b/docs/source/pype.plugins.maya.publish.validate_animation_content.rst deleted file mode 100644 index 65191bb957..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_animation_content.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_animation\_content module -============================================================= - -.. automodule:: pype.plugins.maya.publish.validate_animation_content - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_animation_out_set_related_node_ids.rst b/docs/source/pype.plugins.maya.publish.validate_animation_out_set_related_node_ids.rst deleted file mode 100644 index ea289e84ed..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_animation_out_set_related_node_ids.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_animation\_out\_set\_related\_node\_ids module -================================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_animation_out_set_related_node_ids - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_ass_relative_paths.rst b/docs/source/pype.plugins.maya.publish.validate_ass_relative_paths.rst deleted file mode 100644 index f35ef916cc..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_ass_relative_paths.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_ass\_relative\_paths module -=============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_ass_relative_paths - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_assembly_name.rst b/docs/source/pype.plugins.maya.publish.validate_assembly_name.rst deleted file mode 100644 index c8178226b2..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_assembly_name.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_assembly\_name module -========================================================= - -.. automodule:: pype.plugins.maya.publish.validate_assembly_name - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_assembly_namespaces.rst b/docs/source/pype.plugins.maya.publish.validate_assembly_namespaces.rst deleted file mode 100644 index 847b90281e..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_assembly_namespaces.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_assembly\_namespaces module -=============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_assembly_namespaces - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_assembly_transforms.rst b/docs/source/pype.plugins.maya.publish.validate_assembly_transforms.rst deleted file mode 100644 index b4348a2908..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_assembly_transforms.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_assembly\_transforms module -=============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_assembly_transforms - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_attributes.rst b/docs/source/pype.plugins.maya.publish.validate_attributes.rst deleted file mode 100644 index 862820a7c0..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_attributes.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_attributes module -===================================================== - -.. automodule:: pype.plugins.maya.publish.validate_attributes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_camera_attributes.rst b/docs/source/pype.plugins.maya.publish.validate_camera_attributes.rst deleted file mode 100644 index 054198f812..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_camera_attributes.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_camera\_attributes module -============================================================= - -.. automodule:: pype.plugins.maya.publish.validate_camera_attributes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_camera_contents.rst b/docs/source/pype.plugins.maya.publish.validate_camera_contents.rst deleted file mode 100644 index 9cf6604f7a..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_camera_contents.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_camera\_contents module -=========================================================== - -.. automodule:: pype.plugins.maya.publish.validate_camera_contents - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_color_sets.rst b/docs/source/pype.plugins.maya.publish.validate_color_sets.rst deleted file mode 100644 index 59bb5607bf..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_color_sets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_color\_sets module -====================================================== - -.. automodule:: pype.plugins.maya.publish.validate_color_sets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_current_renderlayer_renderable.rst b/docs/source/pype.plugins.maya.publish.validate_current_renderlayer_renderable.rst deleted file mode 100644 index 31c52477aa..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_current_renderlayer_renderable.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_current\_renderlayer\_renderable module -=========================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_current_renderlayer_renderable - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_deadline_connection.rst b/docs/source/pype.plugins.maya.publish.validate_deadline_connection.rst deleted file mode 100644 index 3f8c4b6313..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_deadline_connection.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_deadline\_connection module -=============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_deadline_connection - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_frame_range.rst b/docs/source/pype.plugins.maya.publish.validate_frame_range.rst deleted file mode 100644 index 0ccc8ed1cd..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_frame_range.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_frame\_range module -======================================================= - -.. automodule:: pype.plugins.maya.publish.validate_frame_range - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_instance_has_members.rst b/docs/source/pype.plugins.maya.publish.validate_instance_has_members.rst deleted file mode 100644 index 862d32f114..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_instance_has_members.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_instance\_has\_members module -================================================================= - -.. automodule:: pype.plugins.maya.publish.validate_instance_has_members - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_instance_subset.rst b/docs/source/pype.plugins.maya.publish.validate_instance_subset.rst deleted file mode 100644 index f71febb73c..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_instance_subset.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_instance\_subset module -=========================================================== - -.. automodule:: pype.plugins.maya.publish.validate_instance_subset - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_instancer_content.rst b/docs/source/pype.plugins.maya.publish.validate_instancer_content.rst deleted file mode 100644 index 761889dd4d..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_instancer_content.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_instancer\_content module -============================================================= - -.. automodule:: pype.plugins.maya.publish.validate_instancer_content - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_instancer_frame_ranges.rst b/docs/source/pype.plugins.maya.publish.validate_instancer_frame_ranges.rst deleted file mode 100644 index 85338c3e2d..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_instancer_frame_ranges.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_instancer\_frame\_ranges module -=================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_instancer_frame_ranges - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_joints_hidden.rst b/docs/source/pype.plugins.maya.publish.validate_joints_hidden.rst deleted file mode 100644 index ede5af0c67..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_joints_hidden.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_joints\_hidden module -========================================================= - -.. automodule:: pype.plugins.maya.publish.validate_joints_hidden - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_look_contents.rst b/docs/source/pype.plugins.maya.publish.validate_look_contents.rst deleted file mode 100644 index 946f924fb3..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_look_contents.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_look\_contents module -========================================================= - -.. automodule:: pype.plugins.maya.publish.validate_look_contents - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_look_default_shaders_connections.rst b/docs/source/pype.plugins.maya.publish.validate_look_default_shaders_connections.rst deleted file mode 100644 index e293cfc0f1..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_look_default_shaders_connections.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_look\_default\_shaders\_connections module -============================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_look_default_shaders_connections - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_look_id_reference_edits.rst b/docs/source/pype.plugins.maya.publish.validate_look_id_reference_edits.rst deleted file mode 100644 index 007f4e2d03..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_look_id_reference_edits.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_look\_id\_reference\_edits module -===================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_look_id_reference_edits - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_look_members_unique.rst b/docs/source/pype.plugins.maya.publish.validate_look_members_unique.rst deleted file mode 100644 index 3378e8a0f6..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_look_members_unique.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_look\_members\_unique module -================================================================ - -.. automodule:: pype.plugins.maya.publish.validate_look_members_unique - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_look_no_default_shaders.rst b/docs/source/pype.plugins.maya.publish.validate_look_no_default_shaders.rst deleted file mode 100644 index 662e2c7621..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_look_no_default_shaders.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_look\_no\_default\_shaders module -===================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_look_no_default_shaders - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_look_sets.rst b/docs/source/pype.plugins.maya.publish.validate_look_sets.rst deleted file mode 100644 index 5427331568..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_look_sets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_look\_sets module -===================================================== - -.. automodule:: pype.plugins.maya.publish.validate_look_sets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_look_shading_group.rst b/docs/source/pype.plugins.maya.publish.validate_look_shading_group.rst deleted file mode 100644 index 259f4952b7..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_look_shading_group.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_look\_shading\_group module -=============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_look_shading_group - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_look_single_shader.rst b/docs/source/pype.plugins.maya.publish.validate_look_single_shader.rst deleted file mode 100644 index fa43283416..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_look_single_shader.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_look\_single\_shader module -=============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_look_single_shader - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_maya_units.rst b/docs/source/pype.plugins.maya.publish.validate_maya_units.rst deleted file mode 100644 index 16af19f6d9..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_maya_units.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_maya\_units module -====================================================== - -.. automodule:: pype.plugins.maya.publish.validate_maya_units - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_arnold_attributes.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_arnold_attributes.rst deleted file mode 100644 index ef18ad1457..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_arnold_attributes.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_arnold\_attributes module -=================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_mesh_arnold_attributes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_has_uv.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_has_uv.rst deleted file mode 100644 index c6af7063c3..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_has_uv.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_has\_uv module -======================================================== - -.. automodule:: pype.plugins.maya.publish.validate_mesh_has_uv - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_lamina_faces.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_lamina_faces.rst deleted file mode 100644 index 006488e77f..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_lamina_faces.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_lamina\_faces module -============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_mesh_lamina_faces - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_no_negative_scale.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_no_negative_scale.rst deleted file mode 100644 index 8720f3d018..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_no_negative_scale.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_no\_negative\_scale module -==================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_mesh_no_negative_scale - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_non_manifold.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_non_manifold.rst deleted file mode 100644 index a69a4c6fc4..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_non_manifold.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_non\_manifold module -============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_mesh_non_manifold - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_non_zero_edge.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_non_zero_edge.rst deleted file mode 100644 index 89ea60d1bc..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_non_zero_edge.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_non\_zero\_edge module -================================================================ - -.. automodule:: pype.plugins.maya.publish.validate_mesh_non_zero_edge - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_normals_unlocked.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_normals_unlocked.rst deleted file mode 100644 index 7dfbd0717d..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_normals_unlocked.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_normals\_unlocked module -================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_mesh_normals_unlocked - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_overlapping_uvs.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_overlapping_uvs.rst deleted file mode 100644 index f5df633124..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_overlapping_uvs.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_overlapping\_uvs module -================================================================= - -.. automodule:: pype.plugins.maya.publish.validate_mesh_overlapping_uvs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_shader_connections.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_shader_connections.rst deleted file mode 100644 index b3cd77ab2a..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_shader_connections.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_shader\_connections module -==================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_mesh_shader_connections - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_single_uv_set.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_single_uv_set.rst deleted file mode 100644 index 29a1217437..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_single_uv_set.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_single\_uv\_set module -================================================================ - -.. automodule:: pype.plugins.maya.publish.validate_mesh_single_uv_set - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_uv_set_map1.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_uv_set_map1.rst deleted file mode 100644 index 49d1b22497..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_uv_set_map1.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_uv\_set\_map1 module -============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_mesh_uv_set_map1 - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_mesh_vertices_have_edges.rst b/docs/source/pype.plugins.maya.publish.validate_mesh_vertices_have_edges.rst deleted file mode 100644 index 99e3047e3d..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_mesh_vertices_have_edges.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_mesh\_vertices\_have\_edges module -====================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_mesh_vertices_have_edges - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_model_content.rst b/docs/source/pype.plugins.maya.publish.validate_model_content.rst deleted file mode 100644 index dc0a415718..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_model_content.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_model\_content module -========================================================= - -.. automodule:: pype.plugins.maya.publish.validate_model_content - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_model_name.rst b/docs/source/pype.plugins.maya.publish.validate_model_name.rst deleted file mode 100644 index ea78ceea70..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_model_name.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_model\_name module -====================================================== - -.. automodule:: pype.plugins.maya.publish.validate_model_name - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_muster_connection.rst b/docs/source/pype.plugins.maya.publish.validate_muster_connection.rst deleted file mode 100644 index 4a4a1e926b..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_muster_connection.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_muster\_connection module -============================================================= - -.. automodule:: pype.plugins.maya.publish.validate_muster_connection - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_no_animation.rst b/docs/source/pype.plugins.maya.publish.validate_no_animation.rst deleted file mode 100644 index b42021369d..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_no_animation.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_no\_animation module -======================================================== - -.. automodule:: pype.plugins.maya.publish.validate_no_animation - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_no_default_camera.rst b/docs/source/pype.plugins.maya.publish.validate_no_default_camera.rst deleted file mode 100644 index 59544369f6..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_no_default_camera.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_no\_default\_camera module -============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_no_default_camera - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_no_namespace.rst b/docs/source/pype.plugins.maya.publish.validate_no_namespace.rst deleted file mode 100644 index bdf4ceb324..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_no_namespace.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_no\_namespace module -======================================================== - -.. automodule:: pype.plugins.maya.publish.validate_no_namespace - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_no_null_transforms.rst b/docs/source/pype.plugins.maya.publish.validate_no_null_transforms.rst deleted file mode 100644 index 12beed8c33..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_no_null_transforms.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_no\_null\_transforms module -=============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_no_null_transforms - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_no_unknown_nodes.rst b/docs/source/pype.plugins.maya.publish.validate_no_unknown_nodes.rst deleted file mode 100644 index 12c977dbb9..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_no_unknown_nodes.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_no\_unknown\_nodes module -============================================================= - -.. automodule:: pype.plugins.maya.publish.validate_no_unknown_nodes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_no_vraymesh.rst b/docs/source/pype.plugins.maya.publish.validate_no_vraymesh.rst deleted file mode 100644 index a1a0b9ee64..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_no_vraymesh.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_no\_vraymesh module -======================================================= - -.. automodule:: pype.plugins.maya.publish.validate_no_vraymesh - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_node_ids.rst b/docs/source/pype.plugins.maya.publish.validate_node_ids.rst deleted file mode 100644 index 7b1d79100f..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_node_ids.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_node\_ids module -==================================================== - -.. automodule:: pype.plugins.maya.publish.validate_node_ids - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_node_ids_deformed_shapes.rst b/docs/source/pype.plugins.maya.publish.validate_node_ids_deformed_shapes.rst deleted file mode 100644 index 90ef81c5b5..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_node_ids_deformed_shapes.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_node\_ids\_deformed\_shapes module -====================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_node_ids_deformed_shapes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_node_ids_in_database.rst b/docs/source/pype.plugins.maya.publish.validate_node_ids_in_database.rst deleted file mode 100644 index 5eb0047d16..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_node_ids_in_database.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_node\_ids\_in\_database module -================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_node_ids_in_database - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_node_ids_related.rst b/docs/source/pype.plugins.maya.publish.validate_node_ids_related.rst deleted file mode 100644 index 1f030462ae..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_node_ids_related.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_node\_ids\_related module -============================================================= - -.. automodule:: pype.plugins.maya.publish.validate_node_ids_related - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_node_ids_unique.rst b/docs/source/pype.plugins.maya.publish.validate_node_ids_unique.rst deleted file mode 100644 index 20ba3a3a6d..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_node_ids_unique.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_node\_ids\_unique module -============================================================ - -.. automodule:: pype.plugins.maya.publish.validate_node_ids_unique - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_node_no_ghosting.rst b/docs/source/pype.plugins.maya.publish.validate_node_no_ghosting.rst deleted file mode 100644 index 8315888630..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_node_no_ghosting.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_node\_no\_ghosting module -============================================================= - -.. automodule:: pype.plugins.maya.publish.validate_node_no_ghosting - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_render_image_rule.rst b/docs/source/pype.plugins.maya.publish.validate_render_image_rule.rst deleted file mode 100644 index 88870a9ea8..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_render_image_rule.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_render\_image\_rule module -============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_render_image_rule - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_render_no_default_cameras.rst b/docs/source/pype.plugins.maya.publish.validate_render_no_default_cameras.rst deleted file mode 100644 index b464dbeab6..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_render_no_default_cameras.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_render\_no\_default\_cameras module -======================================================================= - -.. automodule:: pype.plugins.maya.publish.validate_render_no_default_cameras - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_render_single_camera.rst b/docs/source/pype.plugins.maya.publish.validate_render_single_camera.rst deleted file mode 100644 index 60a0cbd6fb..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_render_single_camera.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_render\_single\_camera module -================================================================= - -.. automodule:: pype.plugins.maya.publish.validate_render_single_camera - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_renderlayer_aovs.rst b/docs/source/pype.plugins.maya.publish.validate_renderlayer_aovs.rst deleted file mode 100644 index 65d5181065..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_renderlayer_aovs.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_renderlayer\_aovs module -============================================================ - -.. automodule:: pype.plugins.maya.publish.validate_renderlayer_aovs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_rendersettings.rst b/docs/source/pype.plugins.maya.publish.validate_rendersettings.rst deleted file mode 100644 index fce7dba5b8..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_rendersettings.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_rendersettings module -========================================================= - -.. automodule:: pype.plugins.maya.publish.validate_rendersettings - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_resources.rst b/docs/source/pype.plugins.maya.publish.validate_resources.rst deleted file mode 100644 index 0a866acdbb..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_resources.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_resources module -==================================================== - -.. automodule:: pype.plugins.maya.publish.validate_resources - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_rig_contents.rst b/docs/source/pype.plugins.maya.publish.validate_rig_contents.rst deleted file mode 100644 index dbd7d84bed..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_rig_contents.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_rig\_contents module -======================================================== - -.. automodule:: pype.plugins.maya.publish.validate_rig_contents - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_rig_controllers.rst b/docs/source/pype.plugins.maya.publish.validate_rig_controllers.rst deleted file mode 100644 index 3bf075e8ad..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_rig_controllers.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_rig\_controllers module -=========================================================== - -.. automodule:: pype.plugins.maya.publish.validate_rig_controllers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_rig_controllers_arnold_attributes.rst b/docs/source/pype.plugins.maya.publish.validate_rig_controllers_arnold_attributes.rst deleted file mode 100644 index 67e9256f3a..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_rig_controllers_arnold_attributes.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_rig\_controllers\_arnold\_attributes module -=============================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_rig_controllers_arnold_attributes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_rig_out_set_node_ids.rst b/docs/source/pype.plugins.maya.publish.validate_rig_out_set_node_ids.rst deleted file mode 100644 index e4f1cfc428..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_rig_out_set_node_ids.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_rig\_out\_set\_node\_ids module -=================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_rig_out_set_node_ids - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_rig_output_ids.rst b/docs/source/pype.plugins.maya.publish.validate_rig_output_ids.rst deleted file mode 100644 index e1d3b1a659..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_rig_output_ids.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_rig\_output\_ids module -=========================================================== - -.. automodule:: pype.plugins.maya.publish.validate_rig_output_ids - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_scene_set_workspace.rst b/docs/source/pype.plugins.maya.publish.validate_scene_set_workspace.rst deleted file mode 100644 index daf2f152d9..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_scene_set_workspace.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_scene\_set\_workspace module -================================================================ - -.. automodule:: pype.plugins.maya.publish.validate_scene_set_workspace - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_shader_name.rst b/docs/source/pype.plugins.maya.publish.validate_shader_name.rst deleted file mode 100644 index ae5b196a1d..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_shader_name.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_shader\_name module -======================================================= - -.. automodule:: pype.plugins.maya.publish.validate_shader_name - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_shape_default_names.rst b/docs/source/pype.plugins.maya.publish.validate_shape_default_names.rst deleted file mode 100644 index 49effc932d..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_shape_default_names.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_shape\_default\_names module -================================================================ - -.. automodule:: pype.plugins.maya.publish.validate_shape_default_names - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_shape_render_stats.rst b/docs/source/pype.plugins.maya.publish.validate_shape_render_stats.rst deleted file mode 100644 index 359af50a0f..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_shape_render_stats.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_shape\_render\_stats module -=============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_shape_render_stats - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_single_assembly.rst b/docs/source/pype.plugins.maya.publish.validate_single_assembly.rst deleted file mode 100644 index 090f57b3ff..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_single_assembly.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_single\_assembly module -=========================================================== - -.. automodule:: pype.plugins.maya.publish.validate_single_assembly - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_skinCluster_deformer_set.rst b/docs/source/pype.plugins.maya.publish.validate_skinCluster_deformer_set.rst deleted file mode 100644 index 607a610097..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_skinCluster_deformer_set.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_skinCluster\_deformer\_set module -===================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_skinCluster_deformer_set - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_step_size.rst b/docs/source/pype.plugins.maya.publish.validate_step_size.rst deleted file mode 100644 index bb883ea7b5..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_step_size.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_step\_size module -===================================================== - -.. automodule:: pype.plugins.maya.publish.validate_step_size - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_transform_naming_suffix.rst b/docs/source/pype.plugins.maya.publish.validate_transform_naming_suffix.rst deleted file mode 100644 index 4d7edda78d..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_transform_naming_suffix.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_transform\_naming\_suffix module -==================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_transform_naming_suffix - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_transform_zero.rst b/docs/source/pype.plugins.maya.publish.validate_transform_zero.rst deleted file mode 100644 index 6d5cacfe00..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_transform_zero.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_transform\_zero module -========================================================== - -.. automodule:: pype.plugins.maya.publish.validate_transform_zero - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_unicode_strings.rst b/docs/source/pype.plugins.maya.publish.validate_unicode_strings.rst deleted file mode 100644 index 9cc17d6810..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_unicode_strings.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_unicode\_strings module -=========================================================== - -.. automodule:: pype.plugins.maya.publish.validate_unicode_strings - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_unreal_mesh_triangulated.rst b/docs/source/pype.plugins.maya.publish.validate_unreal_mesh_triangulated.rst deleted file mode 100644 index 4dcb518194..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_unreal_mesh_triangulated.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_unreal\_mesh\_triangulated module -===================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_unreal_mesh_triangulated - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_unreal_staticmesh_naming.rst b/docs/source/pype.plugins.maya.publish.validate_unreal_staticmesh_naming.rst deleted file mode 100644 index f7225ab395..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_unreal_staticmesh_naming.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_unreal\_staticmesh\_naming module -===================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_unreal_staticmesh_naming - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_unreal_up_axis.rst b/docs/source/pype.plugins.maya.publish.validate_unreal_up_axis.rst deleted file mode 100644 index ff688c493f..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_unreal_up_axis.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_unreal\_up\_axis module -=========================================================== - -.. automodule:: pype.plugins.maya.publish.validate_unreal_up_axis - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_vray_distributed_rendering.rst b/docs/source/pype.plugins.maya.publish.validate_vray_distributed_rendering.rst deleted file mode 100644 index f5d05e6d76..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_vray_distributed_rendering.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_vray\_distributed\_rendering module -======================================================================= - -.. automodule:: pype.plugins.maya.publish.validate_vray_distributed_rendering - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_vray_referenced_aovs.rst b/docs/source/pype.plugins.maya.publish.validate_vray_referenced_aovs.rst deleted file mode 100644 index 16ad9666aa..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_vray_referenced_aovs.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_vray\_referenced\_aovs module -================================================================= - -.. automodule:: pype.plugins.maya.publish.validate_vray_referenced_aovs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_vray_translator_settings.rst b/docs/source/pype.plugins.maya.publish.validate_vray_translator_settings.rst deleted file mode 100644 index a06a9531dd..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_vray_translator_settings.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_vray\_translator\_settings module -===================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_vray_translator_settings - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_vrayproxy.rst b/docs/source/pype.plugins.maya.publish.validate_vrayproxy.rst deleted file mode 100644 index 081f58924a..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_vrayproxy.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_vrayproxy module -==================================================== - -.. automodule:: pype.plugins.maya.publish.validate_vrayproxy - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_vrayproxy_members.rst b/docs/source/pype.plugins.maya.publish.validate_vrayproxy_members.rst deleted file mode 100644 index 7c587f39b0..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_vrayproxy_members.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_vrayproxy\_members module -============================================================= - -.. automodule:: pype.plugins.maya.publish.validate_vrayproxy_members - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_yeti_renderscript_callbacks.rst b/docs/source/pype.plugins.maya.publish.validate_yeti_renderscript_callbacks.rst deleted file mode 100644 index 889d469b2f..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_yeti_renderscript_callbacks.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_yeti\_renderscript\_callbacks module -======================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_yeti_renderscript_callbacks - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_yeti_rig_cache_state.rst b/docs/source/pype.plugins.maya.publish.validate_yeti_rig_cache_state.rst deleted file mode 100644 index 4138b1e8a4..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_yeti_rig_cache_state.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_yeti\_rig\_cache\_state module -================================================================== - -.. automodule:: pype.plugins.maya.publish.validate_yeti_rig_cache_state - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_yeti_rig_input_in_instance.rst b/docs/source/pype.plugins.maya.publish.validate_yeti_rig_input_in_instance.rst deleted file mode 100644 index 37b862926c..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_yeti_rig_input_in_instance.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_yeti\_rig\_input\_in\_instance module -========================================================================= - -.. automodule:: pype.plugins.maya.publish.validate_yeti_rig_input_in_instance - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.publish.validate_yeti_rig_settings.rst b/docs/source/pype.plugins.maya.publish.validate_yeti_rig_settings.rst deleted file mode 100644 index 9fd54193dc..0000000000 --- a/docs/source/pype.plugins.maya.publish.validate_yeti_rig_settings.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.plugins.maya.publish.validate\_yeti\_rig\_settings module -============================================================== - -.. automodule:: pype.plugins.maya.publish.validate_yeti_rig_settings - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.plugins.maya.rst b/docs/source/pype.plugins.maya.rst deleted file mode 100644 index 129cf5fce9..0000000000 --- a/docs/source/pype.plugins.maya.rst +++ /dev/null @@ -1,15 +0,0 @@ -pype.plugins.maya package -========================= - -.. automodule:: pype.plugins.maya - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 10 - - pype.plugins.maya.publish diff --git a/docs/source/pype.plugins.rst b/docs/source/pype.plugins.rst deleted file mode 100644 index 8e5e45ba5d..0000000000 --- a/docs/source/pype.plugins.rst +++ /dev/null @@ -1,15 +0,0 @@ -pype.plugins package -==================== - -.. automodule:: pype.plugins - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 10 - - pype.plugins.maya diff --git a/docs/source/pype.pype_commands.rst b/docs/source/pype.pype_commands.rst deleted file mode 100644 index b8a416df7b..0000000000 --- a/docs/source/pype.pype_commands.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.pype\_commands module -========================== - -.. automodule:: pype.pype_commands - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.resources.rst b/docs/source/pype.resources.rst deleted file mode 100644 index 2fb5b92dce..0000000000 --- a/docs/source/pype.resources.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.resources package -====================== - -.. automodule:: pype.resources - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.rst b/docs/source/pype.rst deleted file mode 100644 index 3589d2f3fe..0000000000 --- a/docs/source/pype.rst +++ /dev/null @@ -1,99 +0,0 @@ -pype package -============ - -.. automodule:: pype - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.hosts - pype.lib - pype.modules - pype.resources - pype.scripts - pype.settings - pype.tests - pype.tools - pype.vendor - pype.widgets - -Submodules ----------- - -pype.action module ------------------- - -.. automodule:: pype.action - :members: - :undoc-members: - :show-inheritance: - -pype.api module ---------------- - -.. automodule:: pype.api - :members: - :undoc-members: - :show-inheritance: - -pype.cli module ---------------- - -.. automodule:: pype.cli - :members: - :undoc-members: - :show-inheritance: - -pype.launcher\_actions module ------------------------------ - -.. automodule:: pype.launcher_actions - :members: - :undoc-members: - :show-inheritance: - -pype.modules\_manager module ----------------------------- - -.. automodule:: pype.modules_manager - :members: - :undoc-members: - :show-inheritance: - -pype.plugin module ------------------- - -.. automodule:: pype.plugin - :members: - :undoc-members: - :show-inheritance: - -pype.pype\_commands module --------------------------- - -.. automodule:: pype.pype_commands - :members: - :undoc-members: - :show-inheritance: - -pype.setdress\_api module -------------------------- - -.. automodule:: pype.setdress_api - :members: - :undoc-members: - :show-inheritance: - -pype.version module -------------------- - -.. automodule:: pype.version - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.export_maya_ass_job.rst b/docs/source/pype.scripts.export_maya_ass_job.rst deleted file mode 100644 index c35cc49ddd..0000000000 --- a/docs/source/pype.scripts.export_maya_ass_job.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.export\_maya\_ass\_job module -========================================== - -.. automodule:: pype.scripts.export_maya_ass_job - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.fusion_switch_shot.rst b/docs/source/pype.scripts.fusion_switch_shot.rst deleted file mode 100644 index 39d3473d16..0000000000 --- a/docs/source/pype.scripts.fusion_switch_shot.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.fusion\_switch\_shot module -======================================== - -.. automodule:: pype.scripts.fusion_switch_shot - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.otio_burnin.rst b/docs/source/pype.scripts.otio_burnin.rst deleted file mode 100644 index e6a93017f5..0000000000 --- a/docs/source/pype.scripts.otio_burnin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.otio\_burnin module -================================ - -.. automodule:: pype.scripts.otio_burnin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.publish_deadline.rst b/docs/source/pype.scripts.publish_deadline.rst deleted file mode 100644 index d134e17244..0000000000 --- a/docs/source/pype.scripts.publish_deadline.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.publish\_deadline module -===================================== - -.. automodule:: pype.scripts.publish_deadline - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.publish_filesequence.rst b/docs/source/pype.scripts.publish_filesequence.rst deleted file mode 100644 index 440d52caad..0000000000 --- a/docs/source/pype.scripts.publish_filesequence.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.publish\_filesequence module -========================================= - -.. automodule:: pype.scripts.publish_filesequence - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.rst b/docs/source/pype.scripts.rst deleted file mode 100644 index 5985771b97..0000000000 --- a/docs/source/pype.scripts.rst +++ /dev/null @@ -1,58 +0,0 @@ -pype.scripts package -==================== - -.. automodule:: pype.scripts - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.scripts.slates - -Submodules ----------- - -pype.scripts.export\_maya\_ass\_job module ------------------------------------------- - -.. automodule:: pype.scripts.export_maya_ass_job - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.fusion\_switch\_shot module ----------------------------------------- - -.. automodule:: pype.scripts.fusion_switch_shot - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.otio\_burnin module --------------------------------- - -.. automodule:: pype.scripts.otio_burnin - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.publish\_deadline module -------------------------------------- - -.. automodule:: pype.scripts.publish_deadline - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.publish\_filesequence module ------------------------------------------ - -.. automodule:: pype.scripts.publish_filesequence - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.slates.rst b/docs/source/pype.scripts.slates.rst deleted file mode 100644 index 74b4cb4343..0000000000 --- a/docs/source/pype.scripts.slates.rst +++ /dev/null @@ -1,15 +0,0 @@ -pype.scripts.slates package -=========================== - -.. automodule:: pype.scripts.slates - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.scripts.slates.slate_base diff --git a/docs/source/pype.scripts.slates.slate_base.api.rst b/docs/source/pype.scripts.slates.slate_base.api.rst deleted file mode 100644 index 0016a5c42a..0000000000 --- a/docs/source/pype.scripts.slates.slate_base.api.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.slates.slate\_base.api module -========================================== - -.. automodule:: pype.scripts.slates.slate_base.api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.slates.slate_base.base.rst b/docs/source/pype.scripts.slates.slate_base.base.rst deleted file mode 100644 index 5e34d654b0..0000000000 --- a/docs/source/pype.scripts.slates.slate_base.base.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.slates.slate\_base.base module -=========================================== - -.. automodule:: pype.scripts.slates.slate_base.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.slates.slate_base.example.rst b/docs/source/pype.scripts.slates.slate_base.example.rst deleted file mode 100644 index 95ebcc835a..0000000000 --- a/docs/source/pype.scripts.slates.slate_base.example.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.slates.slate\_base.example module -============================================== - -.. automodule:: pype.scripts.slates.slate_base.example - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.slates.slate_base.font_factory.rst b/docs/source/pype.scripts.slates.slate_base.font_factory.rst deleted file mode 100644 index c53efef554..0000000000 --- a/docs/source/pype.scripts.slates.slate_base.font_factory.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.slates.slate\_base.font\_factory module -==================================================== - -.. automodule:: pype.scripts.slates.slate_base.font_factory - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.slates.slate_base.items.rst b/docs/source/pype.scripts.slates.slate_base.items.rst deleted file mode 100644 index 25abb11bb9..0000000000 --- a/docs/source/pype.scripts.slates.slate_base.items.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.slates.slate\_base.items module -============================================ - -.. automodule:: pype.scripts.slates.slate_base.items - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.slates.slate_base.layer.rst b/docs/source/pype.scripts.slates.slate_base.layer.rst deleted file mode 100644 index 8681e3accf..0000000000 --- a/docs/source/pype.scripts.slates.slate_base.layer.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.slates.slate\_base.layer module -============================================ - -.. automodule:: pype.scripts.slates.slate_base.layer - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.slates.slate_base.lib.rst b/docs/source/pype.scripts.slates.slate_base.lib.rst deleted file mode 100644 index c4ef2c912e..0000000000 --- a/docs/source/pype.scripts.slates.slate_base.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.slates.slate\_base.lib module -========================================== - -.. automodule:: pype.scripts.slates.slate_base.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.slates.slate_base.main_frame.rst b/docs/source/pype.scripts.slates.slate_base.main_frame.rst deleted file mode 100644 index 5093c28a74..0000000000 --- a/docs/source/pype.scripts.slates.slate_base.main_frame.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.scripts.slates.slate\_base.main\_frame module -================================================== - -.. automodule:: pype.scripts.slates.slate_base.main_frame - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.scripts.slates.slate_base.rst b/docs/source/pype.scripts.slates.slate_base.rst deleted file mode 100644 index 00726c04bf..0000000000 --- a/docs/source/pype.scripts.slates.slate_base.rst +++ /dev/null @@ -1,74 +0,0 @@ -pype.scripts.slates.slate\_base package -======================================= - -.. automodule:: pype.scripts.slates.slate_base - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.scripts.slates.slate\_base.api module ------------------------------------------- - -.. automodule:: pype.scripts.slates.slate_base.api - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.slates.slate\_base.base module -------------------------------------------- - -.. automodule:: pype.scripts.slates.slate_base.base - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.slates.slate\_base.example module ----------------------------------------------- - -.. automodule:: pype.scripts.slates.slate_base.example - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.slates.slate\_base.font\_factory module ----------------------------------------------------- - -.. automodule:: pype.scripts.slates.slate_base.font_factory - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.slates.slate\_base.items module --------------------------------------------- - -.. automodule:: pype.scripts.slates.slate_base.items - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.slates.slate\_base.layer module --------------------------------------------- - -.. automodule:: pype.scripts.slates.slate_base.layer - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.slates.slate\_base.lib module ------------------------------------------- - -.. automodule:: pype.scripts.slates.slate_base.lib - :members: - :undoc-members: - :show-inheritance: - -pype.scripts.slates.slate\_base.main\_frame module --------------------------------------------------- - -.. automodule:: pype.scripts.slates.slate_base.main_frame - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.setdress_api.rst b/docs/source/pype.setdress_api.rst deleted file mode 100644 index 95638ea64d..0000000000 --- a/docs/source/pype.setdress_api.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.setdress\_api module -========================= - -.. automodule:: pype.setdress_api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.settings.constants.rst b/docs/source/pype.settings.constants.rst deleted file mode 100644 index ac652089c8..0000000000 --- a/docs/source/pype.settings.constants.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.settings.constants module -============================== - -.. automodule:: pype.settings.constants - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.settings.handlers.rst b/docs/source/pype.settings.handlers.rst deleted file mode 100644 index 60ea0ae952..0000000000 --- a/docs/source/pype.settings.handlers.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.settings.handlers module -============================= - -.. automodule:: pype.settings.handlers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.settings.lib.rst b/docs/source/pype.settings.lib.rst deleted file mode 100644 index d6e3e8bd06..0000000000 --- a/docs/source/pype.settings.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.settings.lib module -======================== - -.. automodule:: pype.settings.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.settings.rst b/docs/source/pype.settings.rst deleted file mode 100644 index 5bf131d555..0000000000 --- a/docs/source/pype.settings.rst +++ /dev/null @@ -1,18 +0,0 @@ -pype.settings package -===================== - -.. automodule:: pype.settings - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.settings.lib module ------------------------- - -.. automodule:: pype.settings.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tests.lib.rst b/docs/source/pype.tests.lib.rst deleted file mode 100644 index 375ebd0258..0000000000 --- a/docs/source/pype.tests.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tests.lib module -===================== - -.. automodule:: pype.tests.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tests.rst b/docs/source/pype.tests.rst deleted file mode 100644 index 3f34cdcd77..0000000000 --- a/docs/source/pype.tests.rst +++ /dev/null @@ -1,42 +0,0 @@ -pype.tests package -================== - -.. automodule:: pype.tests - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.tests.lib module ---------------------- - -.. automodule:: pype.tests.lib - :members: - :undoc-members: - :show-inheritance: - -pype.tests.test\_avalon\_plugin\_presets module ------------------------------------------------ - -.. automodule:: pype.tests.test_avalon_plugin_presets - :members: - :undoc-members: - :show-inheritance: - -pype.tests.test\_mongo\_performance module ------------------------------------------- - -.. automodule:: pype.tests.test_mongo_performance - :members: - :undoc-members: - :show-inheritance: - -pype.tests.test\_pyblish\_filter module ---------------------------------------- - -.. automodule:: pype.tests.test_pyblish_filter - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tests.test_avalon_plugin_presets.rst b/docs/source/pype.tests.test_avalon_plugin_presets.rst deleted file mode 100644 index b4ff802256..0000000000 --- a/docs/source/pype.tests.test_avalon_plugin_presets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tests.test\_avalon\_plugin\_presets module -=============================================== - -.. automodule:: pype.tests.test_avalon_plugin_presets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tests.test_lib_restructuralization.rst b/docs/source/pype.tests.test_lib_restructuralization.rst deleted file mode 100644 index 8d426fcb6b..0000000000 --- a/docs/source/pype.tests.test_lib_restructuralization.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tests.test\_lib\_restructuralization module -================================================ - -.. automodule:: pype.tests.test_lib_restructuralization - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tests.test_mongo_performance.rst b/docs/source/pype.tests.test_mongo_performance.rst deleted file mode 100644 index 4686247e59..0000000000 --- a/docs/source/pype.tests.test_mongo_performance.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tests.test\_mongo\_performance module -========================================== - -.. automodule:: pype.tests.test_mongo_performance - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tests.test_pyblish_filter.rst b/docs/source/pype.tests.test_pyblish_filter.rst deleted file mode 100644 index 196ec02433..0000000000 --- a/docs/source/pype.tests.test_pyblish_filter.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tests.test\_pyblish\_filter module -======================================= - -.. automodule:: pype.tests.test_pyblish_filter - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.assetcreator.app.rst b/docs/source/pype.tools.assetcreator.app.rst deleted file mode 100644 index b46281b07a..0000000000 --- a/docs/source/pype.tools.assetcreator.app.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.assetcreator.app module -================================== - -.. automodule:: pype.tools.assetcreator.app - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.assetcreator.model.rst b/docs/source/pype.tools.assetcreator.model.rst deleted file mode 100644 index 752791d07c..0000000000 --- a/docs/source/pype.tools.assetcreator.model.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.assetcreator.model module -==================================== - -.. automodule:: pype.tools.assetcreator.model - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.assetcreator.rst b/docs/source/pype.tools.assetcreator.rst deleted file mode 100644 index b95c3b3c60..0000000000 --- a/docs/source/pype.tools.assetcreator.rst +++ /dev/null @@ -1,34 +0,0 @@ -pype.tools.assetcreator package -=============================== - -.. automodule:: pype.tools.assetcreator - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.tools.assetcreator.app module ----------------------------------- - -.. automodule:: pype.tools.assetcreator.app - :members: - :undoc-members: - :show-inheritance: - -pype.tools.assetcreator.model module ------------------------------------- - -.. automodule:: pype.tools.assetcreator.model - :members: - :undoc-members: - :show-inheritance: - -pype.tools.assetcreator.widget module -------------------------------------- - -.. automodule:: pype.tools.assetcreator.widget - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.assetcreator.widget.rst b/docs/source/pype.tools.assetcreator.widget.rst deleted file mode 100644 index 23ed335306..0000000000 --- a/docs/source/pype.tools.assetcreator.widget.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.assetcreator.widget module -===================================== - -.. automodule:: pype.tools.assetcreator.widget - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.launcher.actions.rst b/docs/source/pype.tools.launcher.actions.rst deleted file mode 100644 index e2ec217d4b..0000000000 --- a/docs/source/pype.tools.launcher.actions.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.launcher.actions module -================================== - -.. automodule:: pype.tools.launcher.actions - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.launcher.delegates.rst b/docs/source/pype.tools.launcher.delegates.rst deleted file mode 100644 index e8a7519cd5..0000000000 --- a/docs/source/pype.tools.launcher.delegates.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.launcher.delegates module -==================================== - -.. automodule:: pype.tools.launcher.delegates - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.launcher.flickcharm.rst b/docs/source/pype.tools.launcher.flickcharm.rst deleted file mode 100644 index 5105d3235e..0000000000 --- a/docs/source/pype.tools.launcher.flickcharm.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.launcher.flickcharm module -===================================== - -.. automodule:: pype.tools.launcher.flickcharm - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.launcher.lib.rst b/docs/source/pype.tools.launcher.lib.rst deleted file mode 100644 index 28db8a6540..0000000000 --- a/docs/source/pype.tools.launcher.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.launcher.lib module -============================== - -.. automodule:: pype.tools.launcher.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.launcher.models.rst b/docs/source/pype.tools.launcher.models.rst deleted file mode 100644 index 701826284e..0000000000 --- a/docs/source/pype.tools.launcher.models.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.launcher.models module -================================= - -.. automodule:: pype.tools.launcher.models - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.launcher.rst b/docs/source/pype.tools.launcher.rst deleted file mode 100644 index c4782bf9bb..0000000000 --- a/docs/source/pype.tools.launcher.rst +++ /dev/null @@ -1,66 +0,0 @@ -pype.tools.launcher package -=========================== - -.. automodule:: pype.tools.launcher - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.tools.launcher.actions module ----------------------------------- - -.. automodule:: pype.tools.launcher.actions - :members: - :undoc-members: - :show-inheritance: - -pype.tools.launcher.delegates module ------------------------------------- - -.. automodule:: pype.tools.launcher.delegates - :members: - :undoc-members: - :show-inheritance: - -pype.tools.launcher.flickcharm module -------------------------------------- - -.. automodule:: pype.tools.launcher.flickcharm - :members: - :undoc-members: - :show-inheritance: - -pype.tools.launcher.lib module ------------------------------- - -.. automodule:: pype.tools.launcher.lib - :members: - :undoc-members: - :show-inheritance: - -pype.tools.launcher.models module ---------------------------------- - -.. automodule:: pype.tools.launcher.models - :members: - :undoc-members: - :show-inheritance: - -pype.tools.launcher.widgets module ----------------------------------- - -.. automodule:: pype.tools.launcher.widgets - :members: - :undoc-members: - :show-inheritance: - -pype.tools.launcher.window module ---------------------------------- - -.. automodule:: pype.tools.launcher.window - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.launcher.widgets.rst b/docs/source/pype.tools.launcher.widgets.rst deleted file mode 100644 index 400a5b7a2c..0000000000 --- a/docs/source/pype.tools.launcher.widgets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.launcher.widgets module -================================== - -.. automodule:: pype.tools.launcher.widgets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.launcher.window.rst b/docs/source/pype.tools.launcher.window.rst deleted file mode 100644 index ae92207795..0000000000 --- a/docs/source/pype.tools.launcher.window.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.launcher.window module -================================= - -.. automodule:: pype.tools.launcher.window - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.app.rst b/docs/source/pype.tools.pyblish_pype.app.rst deleted file mode 100644 index a70aada725..0000000000 --- a/docs/source/pype.tools.pyblish_pype.app.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.app module -=================================== - -.. automodule:: pype.tools.pyblish_pype.app - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.awesome.rst b/docs/source/pype.tools.pyblish_pype.awesome.rst deleted file mode 100644 index 50a81ac5e8..0000000000 --- a/docs/source/pype.tools.pyblish_pype.awesome.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.awesome module -======================================= - -.. automodule:: pype.tools.pyblish_pype.awesome - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.compat.rst b/docs/source/pype.tools.pyblish_pype.compat.rst deleted file mode 100644 index 4beee41e00..0000000000 --- a/docs/source/pype.tools.pyblish_pype.compat.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.compat module -====================================== - -.. automodule:: pype.tools.pyblish_pype.compat - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.constants.rst b/docs/source/pype.tools.pyblish_pype.constants.rst deleted file mode 100644 index bab67a2270..0000000000 --- a/docs/source/pype.tools.pyblish_pype.constants.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.constants module -========================================= - -.. automodule:: pype.tools.pyblish_pype.constants - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.control.rst b/docs/source/pype.tools.pyblish_pype.control.rst deleted file mode 100644 index c2f8c0031e..0000000000 --- a/docs/source/pype.tools.pyblish_pype.control.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.control module -======================================= - -.. automodule:: pype.tools.pyblish_pype.control - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.delegate.rst b/docs/source/pype.tools.pyblish_pype.delegate.rst deleted file mode 100644 index 8796c9830f..0000000000 --- a/docs/source/pype.tools.pyblish_pype.delegate.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.delegate module -======================================== - -.. automodule:: pype.tools.pyblish_pype.delegate - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.mock.rst b/docs/source/pype.tools.pyblish_pype.mock.rst deleted file mode 100644 index 8c22e80856..0000000000 --- a/docs/source/pype.tools.pyblish_pype.mock.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.mock module -==================================== - -.. automodule:: pype.tools.pyblish_pype.mock - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.model.rst b/docs/source/pype.tools.pyblish_pype.model.rst deleted file mode 100644 index 983b06cc8a..0000000000 --- a/docs/source/pype.tools.pyblish_pype.model.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.model module -===================================== - -.. automodule:: pype.tools.pyblish_pype.model - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.rst b/docs/source/pype.tools.pyblish_pype.rst deleted file mode 100644 index 9479b5399f..0000000000 --- a/docs/source/pype.tools.pyblish_pype.rst +++ /dev/null @@ -1,130 +0,0 @@ -pype.tools.pyblish\_pype package -================================ - -.. automodule:: pype.tools.pyblish_pype - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.tools.pyblish_pype.vendor - -Submodules ----------- - -pype.tools.pyblish\_pype.app module ------------------------------------ - -.. automodule:: pype.tools.pyblish_pype.app - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.awesome module ---------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.awesome - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.compat module --------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.compat - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.constants module ------------------------------------------ - -.. automodule:: pype.tools.pyblish_pype.constants - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.control module ---------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.control - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.delegate module ----------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.delegate - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.mock module ------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.mock - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.model module -------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.model - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.settings module ----------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.settings - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.util module ------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.util - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.version module ---------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.version - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.view module ------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.view - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.widgets module ---------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.widgets - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.window module --------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.window - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.settings.rst b/docs/source/pype.tools.pyblish_pype.settings.rst deleted file mode 100644 index 2e4e95cca0..0000000000 --- a/docs/source/pype.tools.pyblish_pype.settings.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.settings module -======================================== - -.. automodule:: pype.tools.pyblish_pype.settings - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.util.rst b/docs/source/pype.tools.pyblish_pype.util.rst deleted file mode 100644 index fa34295f12..0000000000 --- a/docs/source/pype.tools.pyblish_pype.util.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.util module -==================================== - -.. automodule:: pype.tools.pyblish_pype.util - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.vendor.qtawesome.animation.rst b/docs/source/pype.tools.pyblish_pype.vendor.qtawesome.animation.rst deleted file mode 100644 index a892128308..0000000000 --- a/docs/source/pype.tools.pyblish_pype.vendor.qtawesome.animation.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.vendor.qtawesome.animation module -========================================================== - -.. automodule:: pype.tools.pyblish_pype.vendor.qtawesome.animation - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.vendor.qtawesome.iconic_font.rst b/docs/source/pype.tools.pyblish_pype.vendor.qtawesome.iconic_font.rst deleted file mode 100644 index 4f4337348f..0000000000 --- a/docs/source/pype.tools.pyblish_pype.vendor.qtawesome.iconic_font.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.vendor.qtawesome.iconic\_font module -============================================================= - -.. automodule:: pype.tools.pyblish_pype.vendor.qtawesome.iconic_font - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.vendor.qtawesome.rst b/docs/source/pype.tools.pyblish_pype.vendor.qtawesome.rst deleted file mode 100644 index 68b2ec4659..0000000000 --- a/docs/source/pype.tools.pyblish_pype.vendor.qtawesome.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.tools.pyblish\_pype.vendor.qtawesome package -================================================= - -.. automodule:: pype.tools.pyblish_pype.vendor.qtawesome - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.tools.pyblish\_pype.vendor.qtawesome.animation module ----------------------------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.vendor.qtawesome.animation - :members: - :undoc-members: - :show-inheritance: - -pype.tools.pyblish\_pype.vendor.qtawesome.iconic\_font module -------------------------------------------------------------- - -.. automodule:: pype.tools.pyblish_pype.vendor.qtawesome.iconic_font - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.vendor.rst b/docs/source/pype.tools.pyblish_pype.vendor.rst deleted file mode 100644 index 69e6096053..0000000000 --- a/docs/source/pype.tools.pyblish_pype.vendor.rst +++ /dev/null @@ -1,15 +0,0 @@ -pype.tools.pyblish\_pype.vendor package -======================================= - -.. automodule:: pype.tools.pyblish_pype.vendor - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.tools.pyblish_pype.vendor.qtawesome diff --git a/docs/source/pype.tools.pyblish_pype.version.rst b/docs/source/pype.tools.pyblish_pype.version.rst deleted file mode 100644 index a6ddcd5ce8..0000000000 --- a/docs/source/pype.tools.pyblish_pype.version.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.version module -======================================= - -.. automodule:: pype.tools.pyblish_pype.version - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.view.rst b/docs/source/pype.tools.pyblish_pype.view.rst deleted file mode 100644 index 21d34d9daa..0000000000 --- a/docs/source/pype.tools.pyblish_pype.view.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.view module -==================================== - -.. automodule:: pype.tools.pyblish_pype.view - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.widgets.rst b/docs/source/pype.tools.pyblish_pype.widgets.rst deleted file mode 100644 index 8a0d3c380a..0000000000 --- a/docs/source/pype.tools.pyblish_pype.widgets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.widgets module -======================================= - -.. automodule:: pype.tools.pyblish_pype.widgets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.pyblish_pype.window.rst b/docs/source/pype.tools.pyblish_pype.window.rst deleted file mode 100644 index 10f7b1a36e..0000000000 --- a/docs/source/pype.tools.pyblish_pype.window.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.pyblish\_pype.window module -====================================== - -.. automodule:: pype.tools.pyblish_pype.window - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.rst b/docs/source/pype.tools.rst deleted file mode 100644 index d82ed3384a..0000000000 --- a/docs/source/pype.tools.rst +++ /dev/null @@ -1,19 +0,0 @@ -pype.tools package -================== - -.. automodule:: pype.tools - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.tools.assetcreator - pype.tools.launcher - pype.tools.pyblish_pype - pype.tools.settings - pype.tools.standalonepublish diff --git a/docs/source/pype.tools.settings.rst b/docs/source/pype.tools.settings.rst deleted file mode 100644 index ef54851ab1..0000000000 --- a/docs/source/pype.tools.settings.rst +++ /dev/null @@ -1,15 +0,0 @@ -pype.tools.settings package -=========================== - -.. automodule:: pype.tools.settings - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.tools.settings.settings diff --git a/docs/source/pype.tools.settings.settings.rst b/docs/source/pype.tools.settings.settings.rst deleted file mode 100644 index 793914e1a8..0000000000 --- a/docs/source/pype.tools.settings.settings.rst +++ /dev/null @@ -1,16 +0,0 @@ -pype.tools.settings.settings package -==================================== - -.. automodule:: pype.tools.settings.settings - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.tools.settings.settings.style - pype.tools.settings.settings.widgets diff --git a/docs/source/pype.tools.settings.settings.style.rst b/docs/source/pype.tools.settings.settings.style.rst deleted file mode 100644 index 228322245c..0000000000 --- a/docs/source/pype.tools.settings.settings.style.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.settings.settings.style package -========================================== - -.. automodule:: pype.tools.settings.settings.style - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.settings.settings.widgets.anatomy_types.rst b/docs/source/pype.tools.settings.settings.widgets.anatomy_types.rst deleted file mode 100644 index ca951c82f0..0000000000 --- a/docs/source/pype.tools.settings.settings.widgets.anatomy_types.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.settings.settings.widgets.anatomy\_types module -========================================================== - -.. automodule:: pype.tools.settings.settings.widgets.anatomy_types - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.settings.settings.widgets.base.rst b/docs/source/pype.tools.settings.settings.widgets.base.rst deleted file mode 100644 index 8964d6f628..0000000000 --- a/docs/source/pype.tools.settings.settings.widgets.base.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.settings.settings.widgets.base module -================================================ - -.. automodule:: pype.tools.settings.settings.widgets.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.settings.settings.widgets.item_types.rst b/docs/source/pype.tools.settings.settings.widgets.item_types.rst deleted file mode 100644 index 5e505538a7..0000000000 --- a/docs/source/pype.tools.settings.settings.widgets.item_types.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.settings.settings.widgets.item\_types module -======================================================= - -.. automodule:: pype.tools.settings.settings.widgets.item_types - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.settings.settings.widgets.lib.rst b/docs/source/pype.tools.settings.settings.widgets.lib.rst deleted file mode 100644 index ae100c74b2..0000000000 --- a/docs/source/pype.tools.settings.settings.widgets.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.settings.settings.widgets.lib module -=============================================== - -.. automodule:: pype.tools.settings.settings.widgets.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.settings.settings.widgets.multiselection_combobox.rst b/docs/source/pype.tools.settings.settings.widgets.multiselection_combobox.rst deleted file mode 100644 index 004f2ae21f..0000000000 --- a/docs/source/pype.tools.settings.settings.widgets.multiselection_combobox.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.settings.settings.widgets.multiselection\_combobox module -==================================================================== - -.. automodule:: pype.tools.settings.settings.widgets.multiselection_combobox - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.settings.settings.widgets.rst b/docs/source/pype.tools.settings.settings.widgets.rst deleted file mode 100644 index 8734280a08..0000000000 --- a/docs/source/pype.tools.settings.settings.widgets.rst +++ /dev/null @@ -1,74 +0,0 @@ -pype.tools.settings.settings.widgets package -============================================ - -.. automodule:: pype.tools.settings.settings.widgets - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.tools.settings.settings.widgets.anatomy\_types module ----------------------------------------------------------- - -.. automodule:: pype.tools.settings.settings.widgets.anatomy_types - :members: - :undoc-members: - :show-inheritance: - -pype.tools.settings.settings.widgets.base module ------------------------------------------------- - -.. automodule:: pype.tools.settings.settings.widgets.base - :members: - :undoc-members: - :show-inheritance: - -pype.tools.settings.settings.widgets.item\_types module -------------------------------------------------------- - -.. automodule:: pype.tools.settings.settings.widgets.item_types - :members: - :undoc-members: - :show-inheritance: - -pype.tools.settings.settings.widgets.lib module ------------------------------------------------ - -.. automodule:: pype.tools.settings.settings.widgets.lib - :members: - :undoc-members: - :show-inheritance: - -pype.tools.settings.settings.widgets.multiselection\_combobox module --------------------------------------------------------------------- - -.. automodule:: pype.tools.settings.settings.widgets.multiselection_combobox - :members: - :undoc-members: - :show-inheritance: - -pype.tools.settings.settings.widgets.tests module -------------------------------------------------- - -.. automodule:: pype.tools.settings.settings.widgets.tests - :members: - :undoc-members: - :show-inheritance: - -pype.tools.settings.settings.widgets.widgets module ---------------------------------------------------- - -.. automodule:: pype.tools.settings.settings.widgets.widgets - :members: - :undoc-members: - :show-inheritance: - -pype.tools.settings.settings.widgets.window module --------------------------------------------------- - -.. automodule:: pype.tools.settings.settings.widgets.window - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.settings.settings.widgets.tests.rst b/docs/source/pype.tools.settings.settings.widgets.tests.rst deleted file mode 100644 index fe8d6dabef..0000000000 --- a/docs/source/pype.tools.settings.settings.widgets.tests.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.settings.settings.widgets.tests module -================================================= - -.. automodule:: pype.tools.settings.settings.widgets.tests - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.settings.settings.widgets.widgets.rst b/docs/source/pype.tools.settings.settings.widgets.widgets.rst deleted file mode 100644 index 238e584ac3..0000000000 --- a/docs/source/pype.tools.settings.settings.widgets.widgets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.settings.settings.widgets.widgets module -=================================================== - -.. automodule:: pype.tools.settings.settings.widgets.widgets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.settings.settings.widgets.window.rst b/docs/source/pype.tools.settings.settings.widgets.window.rst deleted file mode 100644 index d67678012f..0000000000 --- a/docs/source/pype.tools.settings.settings.widgets.window.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.settings.settings.widgets.window module -================================================== - -.. automodule:: pype.tools.settings.settings.widgets.window - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.app.rst b/docs/source/pype.tools.standalonepublish.app.rst deleted file mode 100644 index 74776b80fe..0000000000 --- a/docs/source/pype.tools.standalonepublish.app.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.app module -======================================= - -.. automodule:: pype.tools.standalonepublish.app - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.publish.rst b/docs/source/pype.tools.standalonepublish.publish.rst deleted file mode 100644 index 47ad57e7fb..0000000000 --- a/docs/source/pype.tools.standalonepublish.publish.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.publish module -=========================================== - -.. automodule:: pype.tools.standalonepublish.publish - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.rst b/docs/source/pype.tools.standalonepublish.rst deleted file mode 100644 index 5ca8194b61..0000000000 --- a/docs/source/pype.tools.standalonepublish.rst +++ /dev/null @@ -1,34 +0,0 @@ -pype.tools.standalonepublish package -==================================== - -.. automodule:: pype.tools.standalonepublish - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.tools.standalonepublish.widgets - -Submodules ----------- - -pype.tools.standalonepublish.app module ---------------------------------------- - -.. automodule:: pype.tools.standalonepublish.app - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.publish module -------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.publish - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.model_asset.rst b/docs/source/pype.tools.standalonepublish.widgets.model_asset.rst deleted file mode 100644 index 84d0ca2d93..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.model_asset.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.model\_asset module -======================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.model_asset - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.model_filter_proxy_exact_match.rst b/docs/source/pype.tools.standalonepublish.widgets.model_filter_proxy_exact_match.rst deleted file mode 100644 index 0c3ae79b99..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.model_filter_proxy_exact_match.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.model\_filter\_proxy\_exact\_match module -============================================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.model_filter_proxy_exact_match - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.model_filter_proxy_recursive_sort.rst b/docs/source/pype.tools.standalonepublish.widgets.model_filter_proxy_recursive_sort.rst deleted file mode 100644 index b828b75030..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.model_filter_proxy_recursive_sort.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.model\_filter\_proxy\_recursive\_sort module -================================================================================= - -.. automodule:: pype.tools.standalonepublish.widgets.model_filter_proxy_recursive_sort - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.model_node.rst b/docs/source/pype.tools.standalonepublish.widgets.model_node.rst deleted file mode 100644 index 4789b14501..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.model_node.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.model\_node module -======================================================= - -.. automodule:: pype.tools.standalonepublish.widgets.model_node - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.model_tasks_template.rst b/docs/source/pype.tools.standalonepublish.widgets.model_tasks_template.rst deleted file mode 100644 index dbee838530..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.model_tasks_template.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.model\_tasks\_template module -================================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.model_tasks_template - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.model_tree.rst b/docs/source/pype.tools.standalonepublish.widgets.model_tree.rst deleted file mode 100644 index 38eecb095a..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.model_tree.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.model\_tree module -======================================================= - -.. automodule:: pype.tools.standalonepublish.widgets.model_tree - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.model_tree_view_deselectable.rst b/docs/source/pype.tools.standalonepublish.widgets.model_tree_view_deselectable.rst deleted file mode 100644 index 9afb505113..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.model_tree_view_deselectable.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.model\_tree\_view\_deselectable module -=========================================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.model_tree_view_deselectable - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.resources.rst b/docs/source/pype.tools.standalonepublish.widgets.resources.rst deleted file mode 100644 index a0eddae63e..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.resources.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.resources package -====================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.resources - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.rst b/docs/source/pype.tools.standalonepublish.widgets.rst deleted file mode 100644 index 65bbcb62fc..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.rst +++ /dev/null @@ -1,146 +0,0 @@ -pype.tools.standalonepublish.widgets package -============================================ - -.. automodule:: pype.tools.standalonepublish.widgets - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.tools.standalonepublish.widgets.resources - -Submodules ----------- - -pype.tools.standalonepublish.widgets.model\_asset module --------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.model_asset - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.model\_filter\_proxy\_exact\_match module ------------------------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.model_filter_proxy_exact_match - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.model\_filter\_proxy\_recursive\_sort module ---------------------------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.model_filter_proxy_recursive_sort - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.model\_node module -------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.model_node - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.model\_tasks\_template module ------------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.model_tasks_template - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.model\_tree module -------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.model_tree - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.model\_tree\_view\_deselectable module ---------------------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.model_tree_view_deselectable - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.widget\_asset module ---------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.widget_asset - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.widget\_component\_item module -------------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.widget_component_item - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.widget\_components module --------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.widget_components - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.widget\_components\_list module --------------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.widget_components_list - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.widget\_drop\_empty module ---------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.widget_drop_empty - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.widget\_drop\_frame module ---------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.widget_drop_frame - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.widget\_family module ----------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.widget_family - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.widget\_family\_desc module ----------------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.widget_family_desc - :members: - :undoc-members: - :show-inheritance: - -pype.tools.standalonepublish.widgets.widget\_shadow module ----------------------------------------------------------- - -.. automodule:: pype.tools.standalonepublish.widgets.widget_shadow - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.widget_asset.rst b/docs/source/pype.tools.standalonepublish.widgets.widget_asset.rst deleted file mode 100644 index 51a3763628..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.widget_asset.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.widget\_asset module -========================================================= - -.. automodule:: pype.tools.standalonepublish.widgets.widget_asset - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.widget_component_item.rst b/docs/source/pype.tools.standalonepublish.widgets.widget_component_item.rst deleted file mode 100644 index 3495fdf192..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.widget_component_item.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.widget\_component\_item module -=================================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.widget_component_item - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.widget_components.rst b/docs/source/pype.tools.standalonepublish.widgets.widget_components.rst deleted file mode 100644 index be7c19af9f..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.widget_components.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.widget\_components module -============================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.widget_components - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.widget_components_list.rst b/docs/source/pype.tools.standalonepublish.widgets.widget_components_list.rst deleted file mode 100644 index 051efe07fe..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.widget_components_list.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.widget\_components\_list module -==================================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.widget_components_list - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.widget_drop_empty.rst b/docs/source/pype.tools.standalonepublish.widgets.widget_drop_empty.rst deleted file mode 100644 index b5b0a6acac..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.widget_drop_empty.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.widget\_drop\_empty module -=============================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.widget_drop_empty - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.widget_drop_frame.rst b/docs/source/pype.tools.standalonepublish.widgets.widget_drop_frame.rst deleted file mode 100644 index 6b3e3690e0..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.widget_drop_frame.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.widget\_drop\_frame module -=============================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.widget_drop_frame - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.widget_family.rst b/docs/source/pype.tools.standalonepublish.widgets.widget_family.rst deleted file mode 100644 index 24c9d5496f..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.widget_family.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.widget\_family module -========================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.widget_family - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.widget_family_desc.rst b/docs/source/pype.tools.standalonepublish.widgets.widget_family_desc.rst deleted file mode 100644 index 5a7f92177f..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.widget_family_desc.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.widget\_family\_desc module -================================================================ - -.. automodule:: pype.tools.standalonepublish.widgets.widget_family_desc - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.standalonepublish.widgets.widget_shadow.rst b/docs/source/pype.tools.standalonepublish.widgets.widget_shadow.rst deleted file mode 100644 index 19f5c22198..0000000000 --- a/docs/source/pype.tools.standalonepublish.widgets.widget_shadow.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.standalonepublish.widgets.widget\_shadow module -========================================================== - -.. automodule:: pype.tools.standalonepublish.widgets.widget_shadow - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.tray.pype_tray.rst b/docs/source/pype.tools.tray.pype_tray.rst deleted file mode 100644 index 9fc49c5763..0000000000 --- a/docs/source/pype.tools.tray.pype_tray.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.tray.pype\_tray module -================================= - -.. automodule:: pype.tools.tray.pype_tray - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.tray.rst b/docs/source/pype.tools.tray.rst deleted file mode 100644 index b28059d170..0000000000 --- a/docs/source/pype.tools.tray.rst +++ /dev/null @@ -1,15 +0,0 @@ -pype.tools.tray package -======================= - -.. automodule:: pype.tools.tray - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 10 - - pype.tools.tray.pype_tray diff --git a/docs/source/pype.tools.workfiles.app.rst b/docs/source/pype.tools.workfiles.app.rst deleted file mode 100644 index a3a46b8a07..0000000000 --- a/docs/source/pype.tools.workfiles.app.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.workfiles.app module -=============================== - -.. automodule:: pype.tools.workfiles.app - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.workfiles.model.rst b/docs/source/pype.tools.workfiles.model.rst deleted file mode 100644 index 44cea32b97..0000000000 --- a/docs/source/pype.tools.workfiles.model.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.workfiles.model module -================================= - -.. automodule:: pype.tools.workfiles.model - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.tools.workfiles.rst b/docs/source/pype.tools.workfiles.rst deleted file mode 100644 index 147c4cebbe..0000000000 --- a/docs/source/pype.tools.workfiles.rst +++ /dev/null @@ -1,17 +0,0 @@ -pype.tools.workfiles package -============================ - -.. automodule:: pype.tools.workfiles - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 10 - - pype.tools.workfiles.app - pype.tools.workfiles.model - pype.tools.workfiles.view diff --git a/docs/source/pype.tools.workfiles.view.rst b/docs/source/pype.tools.workfiles.view.rst deleted file mode 100644 index acd32ed250..0000000000 --- a/docs/source/pype.tools.workfiles.view.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.tools.workfiles.view module -================================ - -.. automodule:: pype.tools.workfiles.view - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.backports.configparser.helpers.rst b/docs/source/pype.vendor.backports.configparser.helpers.rst deleted file mode 100644 index 8d44d0a8c4..0000000000 --- a/docs/source/pype.vendor.backports.configparser.helpers.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.backports.configparser.helpers module -================================================= - -.. automodule:: pype.vendor.backports.configparser.helpers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.backports.configparser.rst b/docs/source/pype.vendor.backports.configparser.rst deleted file mode 100644 index 4f778a4a87..0000000000 --- a/docs/source/pype.vendor.backports.configparser.rst +++ /dev/null @@ -1,18 +0,0 @@ -pype.vendor.backports.configparser package -========================================== - -.. automodule:: pype.vendor.backports.configparser - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.vendor.backports.configparser.helpers module -------------------------------------------------- - -.. automodule:: pype.vendor.backports.configparser.helpers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.backports.functools_lru_cache.rst b/docs/source/pype.vendor.backports.functools_lru_cache.rst deleted file mode 100644 index 26f2746cec..0000000000 --- a/docs/source/pype.vendor.backports.functools_lru_cache.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.backports.functools\_lru\_cache module -================================================== - -.. automodule:: pype.vendor.backports.functools_lru_cache - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.backports.rst b/docs/source/pype.vendor.backports.rst deleted file mode 100644 index ff9efc29c5..0000000000 --- a/docs/source/pype.vendor.backports.rst +++ /dev/null @@ -1,26 +0,0 @@ -pype.vendor.backports package -============================= - -.. automodule:: pype.vendor.backports - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.vendor.backports.configparser - -Submodules ----------- - -pype.vendor.backports.functools\_lru\_cache module --------------------------------------------------- - -.. automodule:: pype.vendor.backports.functools_lru_cache - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.builtins.rst b/docs/source/pype.vendor.builtins.rst deleted file mode 100644 index e21fb768ed..0000000000 --- a/docs/source/pype.vendor.builtins.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.builtins package -============================ - -.. automodule:: pype.vendor.builtins - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture.rst b/docs/source/pype.vendor.capture.rst deleted file mode 100644 index d42e073fb5..0000000000 --- a/docs/source/pype.vendor.capture.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture module -========================== - -.. automodule:: pype.vendor.capture - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.accordion.rst b/docs/source/pype.vendor.capture_gui.accordion.rst deleted file mode 100644 index cca228f151..0000000000 --- a/docs/source/pype.vendor.capture_gui.accordion.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture\_gui.accordion module -========================================= - -.. automodule:: pype.vendor.capture_gui.accordion - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.app.rst b/docs/source/pype.vendor.capture_gui.app.rst deleted file mode 100644 index 291296834e..0000000000 --- a/docs/source/pype.vendor.capture_gui.app.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture\_gui.app module -=================================== - -.. automodule:: pype.vendor.capture_gui.app - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.colorpicker.rst b/docs/source/pype.vendor.capture_gui.colorpicker.rst deleted file mode 100644 index c9e56500f2..0000000000 --- a/docs/source/pype.vendor.capture_gui.colorpicker.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture\_gui.colorpicker module -=========================================== - -.. automodule:: pype.vendor.capture_gui.colorpicker - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.lib.rst b/docs/source/pype.vendor.capture_gui.lib.rst deleted file mode 100644 index e94a3bd196..0000000000 --- a/docs/source/pype.vendor.capture_gui.lib.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture\_gui.lib module -=================================== - -.. automodule:: pype.vendor.capture_gui.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.plugin.rst b/docs/source/pype.vendor.capture_gui.plugin.rst deleted file mode 100644 index 2e8f58c873..0000000000 --- a/docs/source/pype.vendor.capture_gui.plugin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture\_gui.plugin module -====================================== - -.. automodule:: pype.vendor.capture_gui.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.presets.rst b/docs/source/pype.vendor.capture_gui.presets.rst deleted file mode 100644 index c81b4e1c23..0000000000 --- a/docs/source/pype.vendor.capture_gui.presets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture\_gui.presets module -======================================= - -.. automodule:: pype.vendor.capture_gui.presets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.rst b/docs/source/pype.vendor.capture_gui.rst deleted file mode 100644 index f7efce3501..0000000000 --- a/docs/source/pype.vendor.capture_gui.rst +++ /dev/null @@ -1,82 +0,0 @@ -pype.vendor.capture\_gui package -================================ - -.. automodule:: pype.vendor.capture_gui - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.vendor.capture_gui.vendor - -Submodules ----------- - -pype.vendor.capture\_gui.accordion module ------------------------------------------ - -.. automodule:: pype.vendor.capture_gui.accordion - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.capture\_gui.app module ------------------------------------ - -.. automodule:: pype.vendor.capture_gui.app - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.capture\_gui.colorpicker module -------------------------------------------- - -.. automodule:: pype.vendor.capture_gui.colorpicker - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.capture\_gui.lib module ------------------------------------ - -.. automodule:: pype.vendor.capture_gui.lib - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.capture\_gui.plugin module --------------------------------------- - -.. automodule:: pype.vendor.capture_gui.plugin - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.capture\_gui.presets module ---------------------------------------- - -.. automodule:: pype.vendor.capture_gui.presets - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.capture\_gui.tokens module --------------------------------------- - -.. automodule:: pype.vendor.capture_gui.tokens - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.capture\_gui.version module ---------------------------------------- - -.. automodule:: pype.vendor.capture_gui.version - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.tokens.rst b/docs/source/pype.vendor.capture_gui.tokens.rst deleted file mode 100644 index 9e144a4d37..0000000000 --- a/docs/source/pype.vendor.capture_gui.tokens.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture\_gui.tokens module -====================================== - -.. automodule:: pype.vendor.capture_gui.tokens - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.vendor.Qt.rst b/docs/source/pype.vendor.capture_gui.vendor.Qt.rst deleted file mode 100644 index 447e6dd812..0000000000 --- a/docs/source/pype.vendor.capture_gui.vendor.Qt.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture\_gui.vendor.Qt module -========================================= - -.. automodule:: pype.vendor.capture_gui.vendor.Qt - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.vendor.rst b/docs/source/pype.vendor.capture_gui.vendor.rst deleted file mode 100644 index 0befc4bbb7..0000000000 --- a/docs/source/pype.vendor.capture_gui.vendor.rst +++ /dev/null @@ -1,18 +0,0 @@ -pype.vendor.capture\_gui.vendor package -======================================= - -.. automodule:: pype.vendor.capture_gui.vendor - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.vendor.capture\_gui.vendor.Qt module ------------------------------------------ - -.. automodule:: pype.vendor.capture_gui.vendor.Qt - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.capture_gui.version.rst b/docs/source/pype.vendor.capture_gui.version.rst deleted file mode 100644 index 3f0cfbabfd..0000000000 --- a/docs/source/pype.vendor.capture_gui.version.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.capture\_gui.version module -======================================= - -.. automodule:: pype.vendor.capture_gui.version - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.accessor.base.rst b/docs/source/pype.vendor.ftrack_api_old.accessor.base.rst deleted file mode 100644 index 5155df82aa..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.accessor.base.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.accessor.base module -================================================= - -.. automodule:: pype.vendor.ftrack_api_old.accessor.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.accessor.disk.rst b/docs/source/pype.vendor.ftrack_api_old.accessor.disk.rst deleted file mode 100644 index 3040fe18fd..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.accessor.disk.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.accessor.disk module -================================================= - -.. automodule:: pype.vendor.ftrack_api_old.accessor.disk - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.accessor.rst b/docs/source/pype.vendor.ftrack_api_old.accessor.rst deleted file mode 100644 index 1f7b522460..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.accessor.rst +++ /dev/null @@ -1,34 +0,0 @@ -pype.vendor.ftrack\_api\_old.accessor package -============================================= - -.. automodule:: pype.vendor.ftrack_api_old.accessor - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.vendor.ftrack\_api\_old.accessor.base module -------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.accessor.base - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.accessor.disk module -------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.accessor.disk - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.accessor.server module ---------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.accessor.server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.accessor.server.rst b/docs/source/pype.vendor.ftrack_api_old.accessor.server.rst deleted file mode 100644 index db835f99c4..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.accessor.server.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.accessor.server module -=================================================== - -.. automodule:: pype.vendor.ftrack_api_old.accessor.server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.attribute.rst b/docs/source/pype.vendor.ftrack_api_old.attribute.rst deleted file mode 100644 index 54276ceb2a..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.attribute.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.attribute module -============================================= - -.. automodule:: pype.vendor.ftrack_api_old.attribute - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.cache.rst b/docs/source/pype.vendor.ftrack_api_old.cache.rst deleted file mode 100644 index 396bc5a1cd..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.cache.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.cache module -========================================= - -.. automodule:: pype.vendor.ftrack_api_old.cache - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.collection.rst b/docs/source/pype.vendor.ftrack_api_old.collection.rst deleted file mode 100644 index de911619fc..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.collection.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.collection module -============================================== - -.. automodule:: pype.vendor.ftrack_api_old.collection - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.data.rst b/docs/source/pype.vendor.ftrack_api_old.data.rst deleted file mode 100644 index 2f67185cee..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.data.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.data module -======================================== - -.. automodule:: pype.vendor.ftrack_api_old.data - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.asset_version.rst b/docs/source/pype.vendor.ftrack_api_old.entity.asset_version.rst deleted file mode 100644 index 7ad3d87fd9..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.asset_version.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity.asset\_version module -========================================================= - -.. automodule:: pype.vendor.ftrack_api_old.entity.asset_version - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.base.rst b/docs/source/pype.vendor.ftrack_api_old.entity.base.rst deleted file mode 100644 index b87428f817..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.base.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity.base module -=============================================== - -.. automodule:: pype.vendor.ftrack_api_old.entity.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.component.rst b/docs/source/pype.vendor.ftrack_api_old.entity.component.rst deleted file mode 100644 index 27901ab786..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.component.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity.component module -==================================================== - -.. automodule:: pype.vendor.ftrack_api_old.entity.component - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.factory.rst b/docs/source/pype.vendor.ftrack_api_old.entity.factory.rst deleted file mode 100644 index caada5c3c8..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.factory.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity.factory module -================================================== - -.. automodule:: pype.vendor.ftrack_api_old.entity.factory - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.job.rst b/docs/source/pype.vendor.ftrack_api_old.entity.job.rst deleted file mode 100644 index 6f4ca18323..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.job.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity.job module -============================================== - -.. automodule:: pype.vendor.ftrack_api_old.entity.job - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.location.rst b/docs/source/pype.vendor.ftrack_api_old.entity.location.rst deleted file mode 100644 index 2f0b380349..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.location.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity.location module -=================================================== - -.. automodule:: pype.vendor.ftrack_api_old.entity.location - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.note.rst b/docs/source/pype.vendor.ftrack_api_old.entity.note.rst deleted file mode 100644 index c04e959402..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.note.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity.note module -=============================================== - -.. automodule:: pype.vendor.ftrack_api_old.entity.note - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.project_schema.rst b/docs/source/pype.vendor.ftrack_api_old.entity.project_schema.rst deleted file mode 100644 index 6332a2e523..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.project_schema.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity.project\_schema module -========================================================== - -.. automodule:: pype.vendor.ftrack_api_old.entity.project_schema - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.rst b/docs/source/pype.vendor.ftrack_api_old.entity.rst deleted file mode 100644 index bb43a7621b..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.rst +++ /dev/null @@ -1,82 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity package -=========================================== - -.. automodule:: pype.vendor.ftrack_api_old.entity - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.vendor.ftrack\_api\_old.entity.asset\_version module ---------------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.entity.asset_version - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.entity.base module ------------------------------------------------ - -.. automodule:: pype.vendor.ftrack_api_old.entity.base - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.entity.component module ----------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.entity.component - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.entity.factory module --------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.entity.factory - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.entity.job module ----------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.entity.job - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.entity.location module ---------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.entity.location - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.entity.note module ------------------------------------------------ - -.. automodule:: pype.vendor.ftrack_api_old.entity.note - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.entity.project\_schema module ----------------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.entity.project_schema - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.entity.user module ------------------------------------------------ - -.. automodule:: pype.vendor.ftrack_api_old.entity.user - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.entity.user.rst b/docs/source/pype.vendor.ftrack_api_old.entity.user.rst deleted file mode 100644 index c0fe6574a6..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.entity.user.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.entity.user module -=============================================== - -.. automodule:: pype.vendor.ftrack_api_old.entity.user - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.event.base.rst b/docs/source/pype.vendor.ftrack_api_old.event.base.rst deleted file mode 100644 index 74b86e3bb2..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.event.base.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.event.base module -============================================== - -.. automodule:: pype.vendor.ftrack_api_old.event.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.event.expression.rst b/docs/source/pype.vendor.ftrack_api_old.event.expression.rst deleted file mode 100644 index 860678797b..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.event.expression.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.event.expression module -==================================================== - -.. automodule:: pype.vendor.ftrack_api_old.event.expression - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.event.hub.rst b/docs/source/pype.vendor.ftrack_api_old.event.hub.rst deleted file mode 100644 index d09d52eedf..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.event.hub.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.event.hub module -============================================= - -.. automodule:: pype.vendor.ftrack_api_old.event.hub - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.event.rst b/docs/source/pype.vendor.ftrack_api_old.event.rst deleted file mode 100644 index 2db27bf7f8..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.event.rst +++ /dev/null @@ -1,50 +0,0 @@ -pype.vendor.ftrack\_api\_old.event package -========================================== - -.. automodule:: pype.vendor.ftrack_api_old.event - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.vendor.ftrack\_api\_old.event.base module ----------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.event.base - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.event.expression module ----------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.event.expression - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.event.hub module ---------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.event.hub - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.event.subscriber module ----------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.event.subscriber - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.event.subscription module ------------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.event.subscription - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.event.subscriber.rst b/docs/source/pype.vendor.ftrack_api_old.event.subscriber.rst deleted file mode 100644 index a9bd13aabc..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.event.subscriber.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.event.subscriber module -==================================================== - -.. automodule:: pype.vendor.ftrack_api_old.event.subscriber - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.event.subscription.rst b/docs/source/pype.vendor.ftrack_api_old.event.subscription.rst deleted file mode 100644 index 423fa9a688..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.event.subscription.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.event.subscription module -====================================================== - -.. automodule:: pype.vendor.ftrack_api_old.event.subscription - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.exception.rst b/docs/source/pype.vendor.ftrack_api_old.exception.rst deleted file mode 100644 index 54dbeeac36..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.exception.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.exception module -============================================= - -.. automodule:: pype.vendor.ftrack_api_old.exception - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.formatter.rst b/docs/source/pype.vendor.ftrack_api_old.formatter.rst deleted file mode 100644 index 75a23eefca..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.formatter.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.formatter module -============================================= - -.. automodule:: pype.vendor.ftrack_api_old.formatter - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.inspection.rst b/docs/source/pype.vendor.ftrack_api_old.inspection.rst deleted file mode 100644 index 2b8849b3d0..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.inspection.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.inspection module -============================================== - -.. automodule:: pype.vendor.ftrack_api_old.inspection - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.logging.rst b/docs/source/pype.vendor.ftrack_api_old.logging.rst deleted file mode 100644 index a10fa10c26..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.logging.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.logging module -=========================================== - -.. automodule:: pype.vendor.ftrack_api_old.logging - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.operation.rst b/docs/source/pype.vendor.ftrack_api_old.operation.rst deleted file mode 100644 index a1d9d606f8..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.operation.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.operation module -============================================= - -.. automodule:: pype.vendor.ftrack_api_old.operation - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.plugin.rst b/docs/source/pype.vendor.ftrack_api_old.plugin.rst deleted file mode 100644 index 0f26c705d2..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.plugin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.plugin module -========================================== - -.. automodule:: pype.vendor.ftrack_api_old.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.query.rst b/docs/source/pype.vendor.ftrack_api_old.query.rst deleted file mode 100644 index 5cf5aba0e4..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.query.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.query module -========================================= - -.. automodule:: pype.vendor.ftrack_api_old.query - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.resource_identifier_transformer.base.rst b/docs/source/pype.vendor.ftrack_api_old.resource_identifier_transformer.base.rst deleted file mode 100644 index dccf51ea71..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.resource_identifier_transformer.base.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.resource\_identifier\_transformer.base module -========================================================================== - -.. automodule:: pype.vendor.ftrack_api_old.resource_identifier_transformer.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.resource_identifier_transformer.rst b/docs/source/pype.vendor.ftrack_api_old.resource_identifier_transformer.rst deleted file mode 100644 index 342ecd9321..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.resource_identifier_transformer.rst +++ /dev/null @@ -1,18 +0,0 @@ -pype.vendor.ftrack\_api\_old.resource\_identifier\_transformer package -====================================================================== - -.. automodule:: pype.vendor.ftrack_api_old.resource_identifier_transformer - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.vendor.ftrack\_api\_old.resource\_identifier\_transformer.base module --------------------------------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.resource_identifier_transformer.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.rst b/docs/source/pype.vendor.ftrack_api_old.rst deleted file mode 100644 index 51d0a29357..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.rst +++ /dev/null @@ -1,126 +0,0 @@ -pype.vendor.ftrack\_api\_old package -==================================== - -.. automodule:: pype.vendor.ftrack_api_old - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.vendor.ftrack_api_old.accessor - pype.vendor.ftrack_api_old.entity - pype.vendor.ftrack_api_old.event - pype.vendor.ftrack_api_old.resource_identifier_transformer - pype.vendor.ftrack_api_old.structure - -Submodules ----------- - -pype.vendor.ftrack\_api\_old.attribute module ---------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.attribute - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.cache module ------------------------------------------ - -.. automodule:: pype.vendor.ftrack_api_old.cache - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.collection module ----------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.collection - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.data module ----------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.data - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.exception module ---------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.exception - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.formatter module ---------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.formatter - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.inspection module ----------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.inspection - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.logging module -------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.logging - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.operation module ---------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.operation - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.plugin module ------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.plugin - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.query module ------------------------------------------ - -.. automodule:: pype.vendor.ftrack_api_old.query - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.session module -------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.session - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.symbol module ------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.symbol - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.session.rst b/docs/source/pype.vendor.ftrack_api_old.session.rst deleted file mode 100644 index beecdeb6af..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.session.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.session module -=========================================== - -.. automodule:: pype.vendor.ftrack_api_old.session - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.structure.base.rst b/docs/source/pype.vendor.ftrack_api_old.structure.base.rst deleted file mode 100644 index 617d8aaed7..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.structure.base.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.structure.base module -================================================== - -.. automodule:: pype.vendor.ftrack_api_old.structure.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.structure.entity_id.rst b/docs/source/pype.vendor.ftrack_api_old.structure.entity_id.rst deleted file mode 100644 index ab6fd0997a..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.structure.entity_id.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.structure.entity\_id module -======================================================== - -.. automodule:: pype.vendor.ftrack_api_old.structure.entity_id - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.structure.id.rst b/docs/source/pype.vendor.ftrack_api_old.structure.id.rst deleted file mode 100644 index 6b887b7917..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.structure.id.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.structure.id module -================================================ - -.. automodule:: pype.vendor.ftrack_api_old.structure.id - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.structure.origin.rst b/docs/source/pype.vendor.ftrack_api_old.structure.origin.rst deleted file mode 100644 index 8ad5fbdc11..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.structure.origin.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.structure.origin module -==================================================== - -.. automodule:: pype.vendor.ftrack_api_old.structure.origin - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.structure.rst b/docs/source/pype.vendor.ftrack_api_old.structure.rst deleted file mode 100644 index 2402430589..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.structure.rst +++ /dev/null @@ -1,50 +0,0 @@ -pype.vendor.ftrack\_api\_old.structure package -============================================== - -.. automodule:: pype.vendor.ftrack_api_old.structure - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.vendor.ftrack\_api\_old.structure.base module --------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.structure.base - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.structure.entity\_id module --------------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.structure.entity_id - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.structure.id module ------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.structure.id - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.structure.origin module ----------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.structure.origin - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.ftrack\_api\_old.structure.standard module ------------------------------------------------------- - -.. automodule:: pype.vendor.ftrack_api_old.structure.standard - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.structure.standard.rst b/docs/source/pype.vendor.ftrack_api_old.structure.standard.rst deleted file mode 100644 index 800201084f..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.structure.standard.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.structure.standard module -====================================================== - -.. automodule:: pype.vendor.ftrack_api_old.structure.standard - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.ftrack_api_old.symbol.rst b/docs/source/pype.vendor.ftrack_api_old.symbol.rst deleted file mode 100644 index bc358d374a..0000000000 --- a/docs/source/pype.vendor.ftrack_api_old.symbol.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.ftrack\_api\_old.symbol module -========================================== - -.. automodule:: pype.vendor.ftrack_api_old.symbol - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.pysync.rst b/docs/source/pype.vendor.pysync.rst deleted file mode 100644 index fbe5b33fb7..0000000000 --- a/docs/source/pype.vendor.pysync.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.vendor.pysync module -========================= - -.. automodule:: pype.vendor.pysync - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.vendor.rst b/docs/source/pype.vendor.rst deleted file mode 100644 index 23aa17f7ab..0000000000 --- a/docs/source/pype.vendor.rst +++ /dev/null @@ -1,37 +0,0 @@ -pype.vendor package -=================== - -.. automodule:: pype.vendor - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 6 - - pype.vendor.backports - pype.vendor.builtins - pype.vendor.capture_gui - pype.vendor.ftrack_api_old - -Submodules ----------- - -pype.vendor.capture module --------------------------- - -.. automodule:: pype.vendor.capture - :members: - :undoc-members: - :show-inheritance: - -pype.vendor.pysync module -------------------------- - -.. automodule:: pype.vendor.pysync - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.version.rst b/docs/source/pype.version.rst deleted file mode 100644 index 7ec69dc423..0000000000 --- a/docs/source/pype.version.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.version module -=================== - -.. automodule:: pype.version - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.widgets.message_window.rst b/docs/source/pype.widgets.message_window.rst deleted file mode 100644 index 60be203837..0000000000 --- a/docs/source/pype.widgets.message_window.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.widgets.message\_window module -=================================== - -.. automodule:: pype.widgets.message_window - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.widgets.popup.rst b/docs/source/pype.widgets.popup.rst deleted file mode 100644 index 7186ff48de..0000000000 --- a/docs/source/pype.widgets.popup.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.widgets.popup module -========================= - -.. automodule:: pype.widgets.popup - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.widgets.project_settings.rst b/docs/source/pype.widgets.project_settings.rst deleted file mode 100644 index 9589cf5479..0000000000 --- a/docs/source/pype.widgets.project_settings.rst +++ /dev/null @@ -1,7 +0,0 @@ -pype.widgets.project\_settings module -===================================== - -.. automodule:: pype.widgets.project_settings - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/pype.widgets.rst b/docs/source/pype.widgets.rst deleted file mode 100644 index 1f09318b67..0000000000 --- a/docs/source/pype.widgets.rst +++ /dev/null @@ -1,34 +0,0 @@ -pype.widgets package -==================== - -.. automodule:: pype.widgets - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -pype.widgets.message\_window module ------------------------------------ - -.. automodule:: pype.widgets.message_window - :members: - :undoc-members: - :show-inheritance: - -pype.widgets.popup module -------------------------- - -.. automodule:: pype.widgets.popup - :members: - :undoc-members: - :show-inheritance: - -pype.widgets.project\_settings module -------------------------------------- - -.. automodule:: pype.widgets.project_settings - :members: - :undoc-members: - :show-inheritance: From 39c142655f84135f9ce2d909eae9c05076bcc107 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 30 Jan 2023 12:47:39 +0100 Subject: [PATCH 196/446] :art: templates and fixes --- README.md | 62 ++++----- docs/source/_templates/autoapi/index.rst | 15 ++ .../_templates/autoapi/python/attribute.rst | 1 + .../_templates/autoapi/python/class.rst | 58 ++++++++ .../source/_templates/autoapi/python/data.rst | 37 +++++ .../_templates/autoapi/python/exception.rst | 1 + .../_templates/autoapi/python/function.rst | 15 ++ .../_templates/autoapi/python/method.rst | 19 +++ .../_templates/autoapi/python/module.rst | 114 +++++++++++++++ .../_templates/autoapi/python/package.rst | 1 + .../_templates/autoapi/python/property.rst | 15 ++ docs/source/conf.py | 8 +- poetry.lock | 131 ++++++++++++++---- pyproject.toml | 3 + 14 files changed, 415 insertions(+), 65 deletions(-) create mode 100644 docs/source/_templates/autoapi/index.rst create mode 100644 docs/source/_templates/autoapi/python/attribute.rst create mode 100644 docs/source/_templates/autoapi/python/class.rst create mode 100644 docs/source/_templates/autoapi/python/data.rst create mode 100644 docs/source/_templates/autoapi/python/exception.rst create mode 100644 docs/source/_templates/autoapi/python/function.rst create mode 100644 docs/source/_templates/autoapi/python/method.rst create mode 100644 docs/source/_templates/autoapi/python/module.rst create mode 100644 docs/source/_templates/autoapi/python/package.rst create mode 100644 docs/source/_templates/autoapi/python/property.rst diff --git a/README.md b/README.md index 8757e3db92..6caed8061c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-) OpenPype -==== +======== [![documentation](https://github.com/pypeclub/pype/actions/workflows/documentation.yml/badge.svg)](https://github.com/pypeclub/pype/actions/workflows/documentation.yml) ![GitHub VFX Platform](https://img.shields.io/badge/vfx%20platform-2022-lightgrey?labelColor=303846) @@ -47,7 +47,7 @@ It can be built and ran on all common platforms. We develop and test on the foll For more details on requirements visit [requirements documentation](https://openpype.io/docs/dev_requirements) Building OpenPype -------------- +----------------- To build OpenPype you currently need [Python 3.9](https://www.python.org/downloads/) as we are following [vfx platform](https://vfxplatform.com). Because of some Linux distros comes with newer Python version @@ -67,9 +67,9 @@ git clone --recurse-submodules git@github.com:Pypeclub/OpenPype.git #### To build OpenPype: -1) Run `.\tools\create_env.ps1` to create virtual environment in `.\venv` +1) Run `.\tools\create_env.ps1` to create virtual environment in `.\venv`. 2) Run `.\tools\fetch_thirdparty_libs.ps1` to download third-party dependencies like ffmpeg and oiio. Those will be included in build. -3) Run `.\tools\build.ps1` to build OpenPype executables in `.\build\` +3) Run `.\tools\build.ps1` to build OpenPype executables in `.\build\`. To create distributable OpenPype versions, run `./tools/create_zip.ps1` - that will create zip file with name `openpype-vx.x.x.zip` parsed from current OpenPype repository and @@ -88,38 +88,38 @@ some OpenPype dependencies like [CMake](https://cmake.org/) and **XCode Command Easy way of installing everything necessary is to use [Homebrew](https://brew.sh): 1) Install **Homebrew**: -```sh -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -``` + ```sh + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + ``` 2) Install **cmake**: -```sh -brew install cmake -``` + ```sh + brew install cmake + ``` 3) Install [pyenv](https://github.com/pyenv/pyenv): -```sh -brew install pyenv -echo 'eval "$(pyenv init -)"' >> ~/.zshrc -pyenv init -exec "$SHELL" -PATH=$(pyenv root)/shims:$PATH -``` + ```sh + brew install pyenv + echo 'eval "$(pyenv init -)"' >> ~/.zshrc + pyenv init + exec "$SHELL" + PATH=$(pyenv root)/shims:$PATH + ``` -4) Pull in required Python version 3.9.x -```sh -# install Python build dependences -brew install openssl readline sqlite3 xz zlib +4) Pull in required Python version 3.9.x: + ```sh + # install Python build dependences + brew install openssl readline sqlite3 xz zlib -# replace with up-to-date 3.9.x version -pyenv install 3.9.6 -``` + # replace with up-to-date 3.9.x version + pyenv install 3.9.6 + ``` -5) Set local Python version -```sh -# switch to OpenPype source directory -pyenv local 3.9.6 -``` +5) Set local Python version: + ```sh + # switch to OpenPype source directory + pyenv local 3.9.6 + ``` #### To build OpenPype: @@ -241,7 +241,7 @@ pyenv local 3.9.6 Running OpenPype ------------- +---------------- OpenPype can by executed either from live sources (this repository) or from *"frozen code"* - executables that can be build using steps described above. @@ -289,7 +289,7 @@ To run tests, execute `.\tools\run_tests(.ps1|.sh)`. Developer tools -------------- +--------------- In case you wish to add your own tools to `.\tools` folder without git tracking, it is possible by adding it with `dev_*` suffix (example: `dev_clear_pyc(.ps1|.sh)`). diff --git a/docs/source/_templates/autoapi/index.rst b/docs/source/_templates/autoapi/index.rst new file mode 100644 index 0000000000..95d0ad8911 --- /dev/null +++ b/docs/source/_templates/autoapi/index.rst @@ -0,0 +1,15 @@ +API Reference +============= + +This page contains auto-generated API reference documentation [#f1]_. + +.. toctree:: + :titlesonly: + + {% for page in pages %} + {% if page.top_level_object and page.display %} + {{ page.include_path }} + {% endif %} + {% endfor %} + +.. [#f1] Created with `sphinx-autoapi `_ diff --git a/docs/source/_templates/autoapi/python/attribute.rst b/docs/source/_templates/autoapi/python/attribute.rst new file mode 100644 index 0000000000..ebaba555ad --- /dev/null +++ b/docs/source/_templates/autoapi/python/attribute.rst @@ -0,0 +1 @@ +{% extends "python/data.rst" %} diff --git a/docs/source/_templates/autoapi/python/class.rst b/docs/source/_templates/autoapi/python/class.rst new file mode 100644 index 0000000000..df5edffb62 --- /dev/null +++ b/docs/source/_templates/autoapi/python/class.rst @@ -0,0 +1,58 @@ +{% if obj.display %} +.. py:{{ obj.type }}:: {{ obj.short_name }}{% if obj.args %}({{ obj.args }}){% endif %} +{% for (args, return_annotation) in obj.overloads %} + {{ " " * (obj.type | length) }} {{ obj.short_name }}{% if args %}({{ args }}){% endif %} +{% endfor %} + + + {% if obj.bases %} + {% if "show-inheritance" in autoapi_options %} + Bases: {% for base in obj.bases %}{{ base|link_objs }}{% if not loop.last %}, {% endif %}{% endfor %} + {% endif %} + + + {% if "show-inheritance-diagram" in autoapi_options and obj.bases != ["object"] %} + .. autoapi-inheritance-diagram:: {{ obj.obj["full_name"] }} + :parts: 1 + {% if "private-members" in autoapi_options %} + :private-bases: + {% endif %} + + {% endif %} + {% endif %} + {% if obj.docstring %} + {{ obj.docstring|indent(3) }} + {% endif %} + {% if "inherited-members" in autoapi_options %} + {% set visible_classes = obj.classes|selectattr("display")|list %} + {% else %} + {% set visible_classes = obj.classes|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for klass in visible_classes %} + {{ klass.render()|indent(3) }} + {% endfor %} + {% if "inherited-members" in autoapi_options %} + {% set visible_properties = obj.properties|selectattr("display")|list %} + {% else %} + {% set visible_properties = obj.properties|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for property in visible_properties %} + {{ property.render()|indent(3) }} + {% endfor %} + {% if "inherited-members" in autoapi_options %} + {% set visible_attributes = obj.attributes|selectattr("display")|list %} + {% else %} + {% set visible_attributes = obj.attributes|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for attribute in visible_attributes %} + {{ attribute.render()|indent(3) }} + {% endfor %} + {% if "inherited-members" in autoapi_options %} + {% set visible_methods = obj.methods|selectattr("display")|list %} + {% else %} + {% set visible_methods = obj.methods|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for method in visible_methods %} + {{ method.render()|indent(3) }} + {% endfor %} +{% endif %} diff --git a/docs/source/_templates/autoapi/python/data.rst b/docs/source/_templates/autoapi/python/data.rst new file mode 100644 index 0000000000..3d12b2d0c7 --- /dev/null +++ b/docs/source/_templates/autoapi/python/data.rst @@ -0,0 +1,37 @@ +{% if obj.display %} +.. py:{{ obj.type }}:: {{ obj.name }} + {%- if obj.annotation is not none %} + + :type: {%- if obj.annotation %} {{ obj.annotation }}{%- endif %} + + {%- endif %} + + {%- if obj.value is not none %} + + :value: {% if obj.value is string and obj.value.splitlines()|count > 1 -%} + Multiline-String + + .. raw:: html + +
Show Value + + .. code-block:: python + + """{{ obj.value|indent(width=8,blank=true) }}""" + + .. raw:: html + +
+ + {%- else -%} + {%- if obj.value is string -%} + {{ "%r" % obj.value|string|truncate(100) }} + {%- else -%} + {{ obj.value|string|truncate(100) }} + {%- endif -%} + {%- endif %} + {%- endif %} + + + {{ obj.docstring|indent(3) }} +{% endif %} diff --git a/docs/source/_templates/autoapi/python/exception.rst b/docs/source/_templates/autoapi/python/exception.rst new file mode 100644 index 0000000000..92f3d38fd5 --- /dev/null +++ b/docs/source/_templates/autoapi/python/exception.rst @@ -0,0 +1 @@ +{% extends "python/class.rst" %} diff --git a/docs/source/_templates/autoapi/python/function.rst b/docs/source/_templates/autoapi/python/function.rst new file mode 100644 index 0000000000..b00d5c2445 --- /dev/null +++ b/docs/source/_templates/autoapi/python/function.rst @@ -0,0 +1,15 @@ +{% if obj.display %} +.. py:function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} + +{% for (args, return_annotation) in obj.overloads %} + {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} + +{% endfor %} + {% for property in obj.properties %} + :{{ property }}: + {% endfor %} + + {% if obj.docstring %} + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/source/_templates/autoapi/python/method.rst b/docs/source/_templates/autoapi/python/method.rst new file mode 100644 index 0000000000..723cb7bbe5 --- /dev/null +++ b/docs/source/_templates/autoapi/python/method.rst @@ -0,0 +1,19 @@ +{%- if obj.display %} +.. py:method:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} + +{% for (args, return_annotation) in obj.overloads %} + {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} + +{% endfor %} + {% if obj.properties %} + {% for property in obj.properties %} + :{{ property }}: + {% endfor %} + + {% else %} + + {% endif %} + {% if obj.docstring %} + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/source/_templates/autoapi/python/module.rst b/docs/source/_templates/autoapi/python/module.rst new file mode 100644 index 0000000000..d2714f6c9d --- /dev/null +++ b/docs/source/_templates/autoapi/python/module.rst @@ -0,0 +1,114 @@ +{% if not obj.display %} +:orphan: + +{% endif %} +:py:mod:`{{ obj.name }}` +=========={{ "=" * obj.name|length }} + +.. py:module:: {{ obj.name }} + +{% if obj.docstring %} +.. autoapi-nested-parse:: + + {{ obj.docstring|indent(3) }} + +{% endif %} + +{% block subpackages %} +{% set visible_subpackages = obj.subpackages|selectattr("display")|list %} +{% if visible_subpackages %} +Subpackages +----------- +.. toctree:: + :titlesonly: + :maxdepth: 3 + +{% for subpackage in visible_subpackages %} + {{ subpackage.short_name }}/index.rst +{% endfor %} + + +{% endif %} +{% endblock %} +{% block submodules %} +{% set visible_submodules = obj.submodules|selectattr("display")|list %} +{% if visible_submodules %} +Submodules +---------- +.. toctree:: + :titlesonly: + :maxdepth: 1 + +{% for submodule in visible_submodules %} + {{ submodule.short_name }}/index.rst +{% endfor %} + + +{% endif %} +{% endblock %} +{% block content %} +{% if obj.all is not none %} +{% set visible_children = obj.children|selectattr("short_name", "in", obj.all)|list %} +{% elif obj.type is equalto("package") %} +{% set visible_children = obj.children|selectattr("display")|list %} +{% else %} +{% set visible_children = obj.children|selectattr("display")|rejectattr("imported")|list %} +{% endif %} +{% if visible_children %} +{{ obj.type|title }} Contents +{{ "-" * obj.type|length }}--------- + +{% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %} +{% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %} +{% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %} +{% if "show-module-summary" in autoapi_options and (visible_classes or visible_functions) %} +{% block classes scoped %} +{% if visible_classes %} +Classes +~~~~~~~ + +.. autoapisummary:: + +{% for klass in visible_classes %} + {{ klass.id }} +{% endfor %} + + +{% endif %} +{% endblock %} + +{% block functions scoped %} +{% if visible_functions %} +Functions +~~~~~~~~~ + +.. autoapisummary:: + +{% for function in visible_functions %} + {{ function.id }} +{% endfor %} + + +{% endif %} +{% endblock %} + +{% block attributes scoped %} +{% if visible_attributes %} +Attributes +~~~~~~~~~~ + +.. autoapisummary:: + +{% for attribute in visible_attributes %} + {{ attribute.id }} +{% endfor %} + + +{% endif %} +{% endblock %} +{% endif %} +{% for obj_item in visible_children %} +{{ obj_item.render()|indent(0) }} +{% endfor %} +{% endif %} +{% endblock %} diff --git a/docs/source/_templates/autoapi/python/package.rst b/docs/source/_templates/autoapi/python/package.rst new file mode 100644 index 0000000000..fb9a64965e --- /dev/null +++ b/docs/source/_templates/autoapi/python/package.rst @@ -0,0 +1 @@ +{% extends "python/module.rst" %} diff --git a/docs/source/_templates/autoapi/python/property.rst b/docs/source/_templates/autoapi/python/property.rst new file mode 100644 index 0000000000..70af24236f --- /dev/null +++ b/docs/source/_templates/autoapi/python/property.rst @@ -0,0 +1,15 @@ +{%- if obj.display %} +.. py:property:: {{ obj.short_name }} + {% if obj.annotation %} + :type: {{ obj.annotation }} + {% endif %} + {% if obj.properties %} + {% for property in obj.properties %} + :{{ property }}: + {% endfor %} + {% endif %} + + {% if obj.docstring %} + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/source/conf.py b/docs/source/conf.py index c54f51cbe9..768481d3f9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,11 +17,11 @@ import os import sys -from Qt.QtWidgets import QApplication +# from Qt.QtWidgets import QApplication openpype_root = os.path.abspath('../..') sys.path.insert(0, openpype_root) -app = QApplication([]) +# app = QApplication([]) """ repos = os.listdir(os.path.abspath("../../repos")) @@ -65,7 +65,7 @@ extensions = [ 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary', - 'm2r2' + 'm2r2', 'autoapi.extension' ] @@ -94,7 +94,7 @@ autoapi_options = [ 'show-module-summary' ] autoapi_add_toctree_entry = True -autoapi_template_dir = '_autoapi_templates' +autoapi_template_dir = '_templates/autoapi' # Add any paths that contain templates here, relative to this directory. diff --git a/poetry.lock b/poetry.lock index f71611cb6f..29d9bbaba9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -176,7 +176,7 @@ frozenlist = ">=1.1.0" name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -231,7 +231,7 @@ python-dateutil = ">=2.7.0" name = "astroid" version = "2.13.2" description = "An abstract syntax tree for Python with inference support." -category = "dev" +category = "main" optional = false python-versions = ">=3.7.2" files = [ @@ -286,6 +286,20 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope.interface"] tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] +[[package]] +name = "autoapi" +version = "2.0.1" +description = "Automatic API reference documentation generation for Sphinx inspired by Doxygen" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "autoapi-2.0.1.tar.gz", hash = "sha256:4003b17599020652d0738dc1c426d0abf2f58f8a1821f5500816043210b3d1d6"}, +] + +[package.dependencies] +sphinx = "*" + [[package]] name = "autopep8" version = "2.0.1" @@ -306,7 +320,7 @@ tomli = {version = "*", markers = "python_version < \"3.11\""} name = "babel" version = "2.11.0" description = "Internationalization utilities" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -530,7 +544,7 @@ test = ["pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -835,7 +849,7 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"] name = "docutils" version = "0.19" description = "Docutils -- Python Documentation Utilities" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1238,7 +1252,7 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1334,7 +1348,7 @@ trio = ["async_generator", "trio"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1404,7 +1418,7 @@ testing = ["pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2. name = "lazy-object-proxy" version = "1.9.0" description = "A fast and thorough lazy object proxy." -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1501,11 +1515,27 @@ files = [ [package.dependencies] pymongo = "*" +[[package]] +name = "m2r2" +version = "0.3.3.post2" +description = "Markdown and reStructuredText in a single file." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "m2r2-0.3.3.post2-py3-none-any.whl", hash = "sha256:86157721eb6eabcd54d4eea7195890cc58fa6188b8d0abea633383cfbb5e11e3"}, + {file = "m2r2-0.3.3.post2.tar.gz", hash = "sha256:e62bcb0e74b3ce19cda0737a0556b04cf4a43b785072fcef474558f2c1482ca8"}, +] + +[package.dependencies] +docutils = ">=0.19" +mistune = "0.8.4" + [[package]] name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1592,6 +1622,18 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mistune" +version = "0.8.4" +description = "The fastest markdown parser in pure Python" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, +] + [[package]] name = "multidict" version = "6.0.4" @@ -1867,13 +1909,6 @@ files = [ {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, - {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, - {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, - {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, - {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, - {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, - {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, @@ -2161,7 +2196,7 @@ files = [ name = "pygments" version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2352,7 +2387,7 @@ files = [ cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] @@ -2628,7 +2663,7 @@ files = [ name = "pytz" version = "2022.7.1" description = "World timezone definitions, modern and historical" -category = "dev" +category = "main" optional = false python-versions = "*" files = [ @@ -2672,7 +2707,7 @@ files = [ name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2923,7 +2958,7 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" +category = "main" optional = false python-versions = "*" files = [ @@ -2947,7 +2982,7 @@ files = [ name = "sphinx" version = "6.1.3" description = "Python documentation generator" -category = "dev" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2979,6 +3014,30 @@ docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] test = ["cython", "html5lib", "pytest (>=4.6)"] +[[package]] +name = "sphinx-autoapi" +version = "2.0.1" +description = "Sphinx API documentation generator" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sphinx-autoapi-2.0.1.tar.gz", hash = "sha256:cdf47968c20852f4feb0ccefd09e414bb820af8af8f82fab15a24b09a3d1baba"}, + {file = "sphinx_autoapi-2.0.1-py2.py3-none-any.whl", hash = "sha256:8ed197a0c9108770aa442a5445744c1405b356ea64df848e8553411b9b9e129b"}, +] + +[package.dependencies] +astroid = ">=2.7" +Jinja2 = "*" +PyYAML = "*" +sphinx = ">=4.0" +unidecode = "*" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +dotnet = ["sphinxcontrib-dotnetdomain"] +go = ["sphinxcontrib-golangdomain"] + [[package]] name = "sphinx-rtd-theme" version = "0.5.1" @@ -3001,7 +3060,7 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] name = "sphinxcontrib-applehelp" version = "1.0.3" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -category = "dev" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3017,7 +3076,7 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "dev" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3033,7 +3092,7 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3049,7 +3108,7 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3064,7 +3123,7 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "dev" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3080,7 +3139,7 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "dev" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3160,7 +3219,7 @@ files = [ name = "typing-extensions" version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3168,6 +3227,18 @@ files = [ {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] +[[package]] +name = "unidecode" +version = "1.3.6" +description = "ASCII transliterations of Unicode text" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, + {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, +] + [[package]] name = "uritemplate" version = "3.0.1" @@ -3462,4 +3533,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "02daca205796a0f29a0d9f50707544e6804f32027eba493cd2aa7f175a00dcea" +content-hash = "ffcea2de2e31b6dbcd4797d58a2c2da73f651d205e51c88df7276456d87b0942" diff --git a/pyproject.toml b/pyproject.toml index 1d124574c7..d1ab2af6ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,9 @@ pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" opencolorio = "^2.2.0" +m2r2 = "^0.3.3.post2" +autoapi = "^2.0.1" +sphinx-autoapi = "^2.0.1" [tool.poetry.dev-dependencies] flake8 = "^6.0" From 5506f52f25cf490e43cfcfe9bbe51e88a2f916bf Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 30 Jan 2023 13:58:39 +0100 Subject: [PATCH 197/446] :art: change docs theme --- docs/source/conf.py | 4 +- poetry.lock | 103 +++++++++++++++++++++++++++++++++++++++----- pyproject.toml | 13 +++--- 3 files changed, 103 insertions(+), 17 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 768481d3f9..e7943a47b3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -114,7 +114,7 @@ master_doc = 'index' # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "English" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -140,7 +140,7 @@ autosummary_generate = True # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = 'furo' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/poetry.lock b/poetry.lock index 29d9bbaba9..6f20321892 100644 --- a/poetry.lock +++ b/poetry.lock @@ -366,6 +366,25 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "beautifulsoup4" +version = "4.11.1" +description = "Screen-scraping library" +category = "main" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, + {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "blessed" version = "1.19.1" @@ -1042,6 +1061,24 @@ six = ">=1.13.0,<2" termcolor = ">=1.1.0,<2" websocket-client = ">=0.40.0,<1" +[[package]] +name = "furo" +version = "2022.12.7" +description = "A clean customisable Sphinx documentation theme." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "furo-2022.12.7-py3-none-any.whl", hash = "sha256:7cb76c12a25ef65db85ab0743df907573d03027a33631f17d267e598ebb191f7"}, + {file = "furo-2022.12.7.tar.gz", hash = "sha256:d8008f8efbe7587a97ba533c8b2df1f9c21ee9b3e5cad0d27f61193d38b1a986"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +pygments = ">=2.7" +sphinx = ">=5.0,<7.0" +sphinx-basic-ng = "*" + [[package]] name = "future" version = "0.18.3" @@ -2022,6 +2059,21 @@ files = [ {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, ] +[[package]] +name = "pockets" +version = "0.9.1" +description = "A collection of helpful Python tools!" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pockets-0.9.1-py2.py3-none-any.whl", hash = "sha256:68597934193c08a08eb2bf6a1d85593f627c22f9b065cc727a4f03f669d96d86"}, + {file = "pockets-0.9.1.tar.gz", hash = "sha256:9320f1a3c6f7a9133fe3b571f283bcf3353cd70249025ae8d618e40e9f7e92b3"}, +] + +[package.dependencies] +six = ">=1.5.2" + [[package]] name = "pre-commit" version = "2.21.0" @@ -2966,6 +3018,18 @@ files = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +[[package]] +name = "soupsieve" +version = "2.3.2.post1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, + {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, +] + [[package]] name = "speedcopy" version = "2.1.4" @@ -3039,22 +3103,22 @@ dotnet = ["sphinxcontrib-dotnetdomain"] go = ["sphinxcontrib-golangdomain"] [[package]] -name = "sphinx-rtd-theme" -version = "0.5.1" -description = "Read the Docs theme for Sphinx" -category = "dev" +name = "sphinx-basic-ng" +version = "1.0.0b1" +description = "A modern skeleton for Sphinx themes." +category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl", hash = "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113"}, - {file = "sphinx_rtd_theme-0.5.1.tar.gz", hash = "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5"}, + {file = "sphinx_basic_ng-1.0.0b1-py3-none-any.whl", hash = "sha256:ade597a3029c7865b24ad0eda88318766bcc2f9f4cef60df7e28126fde94db2a"}, + {file = "sphinx_basic_ng-1.0.0b1.tar.gz", hash = "sha256:89374bd3ccd9452a301786781e28c8718e99960f2d4f411845ea75fc7bb5a9b0"}, ] [package.dependencies] -sphinx = "*" +sphinx = ">=4.0" [package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] +docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] [[package]] name = "sphinxcontrib-applehelp" @@ -3119,6 +3183,22 @@ files = [ [package.extras] test = ["flake8", "mypy", "pytest"] +[[package]] +name = "sphinxcontrib-napoleon" +version = "0.7" +description = "Sphinx \"napoleon\" extension." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "sphinxcontrib-napoleon-0.7.tar.gz", hash = "sha256:407382beed396e9f2d7f3043fad6afda95719204a1e1a231ac865f40abcbfcf8"}, + {file = "sphinxcontrib_napoleon-0.7-py2.py3-none-any.whl", hash = "sha256:711e41a3974bdf110a484aec4c1a556799eb0b3f3b897521a018ad7e2db13fef"}, +] + +[package.dependencies] +pockets = ">=0.3" +six = ">=1.5.2" + [[package]] name = "sphinxcontrib-qthelp" version = "1.0.3" @@ -3530,7 +3610,10 @@ files = [ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +[extras] +docs = ["sphinxcontrib-napoleon"] + [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "ffcea2de2e31b6dbcd4797d58a2c2da73f651d205e51c88df7276456d87b0942" +content-hash = "e3b125855e7101db8c9d25fe8d42e45430c3ab1c80e9511c01b72967ba72e6bc" diff --git a/pyproject.toml b/pyproject.toml index d1ab2af6ea..d4b6bdbde0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,9 +71,6 @@ pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" opencolorio = "^2.2.0" -m2r2 = "^0.3.3.post2" -autoapi = "^2.0.1" -sphinx-autoapi = "^2.0.1" [tool.poetry.dev-dependencies] flake8 = "^6.0" @@ -90,8 +87,11 @@ pylint = "^2.4.4" pytest = "^6.1" pytest-cov = "*" pytest-print = "*" -Sphinx = "^6.1" -sphinx-rtd-theme = "*" +Sphinx = { version = "^6.1", optional = true } +m2r2 = { version = "^0.3.3.post2", optional = true } +sphinx-autoapi = { version = "^2.0.1", optional = true } +sphinxcontrib-napoleon = { version = "^0.7", optional = true } +furo = { version = "^2022.12.7", optional = true } recommonmark = "*" wheel = "*" enlighten = "*" # cool terminal progress bars @@ -168,3 +168,6 @@ ignore = ["website", "docs", ".git"] reportMissingImports = true reportMissingTypeStubs = false + +[tool.poetry.extras] +docs = ["Sphinx", "furo", "sphinxcontrib-napoleon"] From 9d8ebf5091fcbd8192ac80e72e6a474cf65cbcc0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 1 Feb 2023 18:11:45 +0100 Subject: [PATCH 198/446] :recycle: theme switch --- docs/make.bat | 2 +- docs/source/_static/README.md | 0 docs/source/conf.py | 26 ++-- docs/source/index.rst | 6 +- docs/source/modules.rst | 2 +- docs/source/readme.rst | 2 +- poetry.lock | 237 +++++++++++++++++++--------------- pyproject.toml | 5 +- 8 files changed, 155 insertions(+), 125 deletions(-) create mode 100644 docs/source/_static/README.md diff --git a/docs/make.bat b/docs/make.bat index 4d9eb83d9f..1d261df277 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -5,7 +5,7 @@ pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build + set SPHINXBUILD=..\.poetry\bin\poetry run sphinx-build ) set SOURCEDIR=source set BUILDDIR=build diff --git a/docs/source/_static/README.md b/docs/source/_static/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/source/conf.py b/docs/source/conf.py index e7943a47b3..b4fe161c2b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,7 +17,7 @@ import os import sys -# from Qt.QtWidgets import QApplication +import revitron_sphinx_theme openpype_root = os.path.abspath('../..') sys.path.insert(0, openpype_root) @@ -65,8 +65,9 @@ extensions = [ 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary', - 'm2r2', - 'autoapi.extension' + 'revitron_sphinx_theme', + 'autoapi.extension', + 'myst_parser' ] ############################## @@ -75,16 +76,17 @@ extensions = [ autoapi_dirs = ['../../openpype', '../../igniter'] -# bypas modules with a lot of python2 content for now +# bypass modules with a lot of python2 content for now autoapi_ignore = [ - "*plugin*", - "*hosts*", "*vendor*", - "*modules*", - "*setup*", - "*tools*", "*schemas*", - "*website*" + "*startup/*", + "*/website*", + "*openpype/hooks*", + "*openpype/style*", + "openpype/tests*", + # to many levels of relative import: + "*/modules/sync_server/*" ] autoapi_keep_files = True autoapi_options = [ @@ -140,7 +142,7 @@ autosummary_generate = True # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'furo' +html_theme = 'revitron_sphinx_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -256,3 +258,5 @@ intersphinx_mapping = { # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True + +myst_gfm_only = True diff --git a/docs/source/index.rst b/docs/source/index.rst index b54d153894..b1ce5f2331 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,14 +1,14 @@ -.. pype documentation master file, created by +.. openpype documentation master file, created by sphinx-quickstart on Mon May 13 17:18:23 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to pype's documentation! +Welcome to OpenPype's API documentation! ================================ .. toctree:: readme - modules + openpype Indices and tables ================== diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 1956d9ed04..a232a7c901 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -5,4 +5,4 @@ igniter :maxdepth: 6 igniter - pype \ No newline at end of file + openpype diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 823c0df3c8..842932f494 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -1,2 +1,2 @@ -.. title:: Pype Readme +.. title:: OpenPype Readme .. include:: ../../README.md diff --git a/poetry.lock b/poetry.lock index 6f20321892..4107ea8335 100644 --- a/poetry.lock +++ b/poetry.lock @@ -231,7 +231,7 @@ python-dateutil = ">=2.7.0" name = "astroid" version = "2.13.2" description = "An abstract syntax tree for Python with inference support." -category = "main" +category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -286,20 +286,6 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope.interface"] tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] -[[package]] -name = "autoapi" -version = "2.0.1" -description = "Automatic API reference documentation generation for Sphinx inspired by Doxygen" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "autoapi-2.0.1.tar.gz", hash = "sha256:4003b17599020652d0738dc1c426d0abf2f58f8a1821f5500816043210b3d1d6"}, -] - -[package.dependencies] -sphinx = "*" - [[package]] name = "autopep8" version = "2.0.1" @@ -366,25 +352,6 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] -[[package]] -name = "beautifulsoup4" -version = "4.11.1" -description = "Screen-scraping library" -category = "main" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, - {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] - [[package]] name = "blessed" version = "1.19.1" @@ -1061,24 +1028,6 @@ six = ">=1.13.0,<2" termcolor = ">=1.1.0,<2" websocket-client = ">=0.40.0,<1" -[[package]] -name = "furo" -version = "2022.12.7" -description = "A clean customisable Sphinx documentation theme." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "furo-2022.12.7-py3-none-any.whl", hash = "sha256:7cb76c12a25ef65db85ab0743df907573d03027a33631f17d267e598ebb191f7"}, - {file = "furo-2022.12.7.tar.gz", hash = "sha256:d8008f8efbe7587a97ba533c8b2df1f9c21ee9b3e5cad0d27f61193d38b1a986"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -pygments = ">=2.7" -sphinx = ">=5.0,<7.0" -sphinx-basic-ng = "*" - [[package]] name = "future" version = "0.18.3" @@ -1455,7 +1404,7 @@ testing = ["pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2. name = "lazy-object-proxy" version = "1.9.0" description = "A fast and thorough lazy object proxy." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1556,8 +1505,8 @@ pymongo = "*" name = "m2r2" version = "0.3.3.post2" description = "Markdown and reStructuredText in a single file." -category = "main" -optional = false +category = "dev" +optional = true python-versions = ">=3.7" files = [ {file = "m2r2-0.3.3.post2-py3-none-any.whl", hash = "sha256:86157721eb6eabcd54d4eea7195890cc58fa6188b8d0abea633383cfbb5e11e3"}, @@ -1568,6 +1517,31 @@ files = [ docutils = ">=0.19" mistune = "0.8.4" +[[package]] +name = "markdown-it-py" +version = "2.1.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, + {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] +code-style = ["pre-commit (==2.6)"] +compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] +linkify = ["linkify-it-py (>=1.0,<2.0)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" version = "2.0.1" @@ -1659,12 +1633,44 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mdit-py-plugins" +version = "0.3.3" +description = "Collection of plugins for markdown-it-py" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdit-py-plugins-0.3.3.tar.gz", hash = "sha256:5cfd7e7ac582a594e23ba6546a2f406e94e42eb33ae596d0734781261c251260"}, + {file = "mdit_py_plugins-0.3.3-py3-none-any.whl", hash = "sha256:36d08a29def19ec43acdcd8ba471d3ebab132e7879d442760d963f19913e04b9"}, +] + +[package.dependencies] +markdown-it-py = ">=1.0.0,<3.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mistune" version = "0.8.4" description = "The fastest markdown parser in pure Python" -category = "main" -optional = false +category = "dev" +optional = true python-versions = "*" files = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, @@ -1755,6 +1761,33 @@ files = [ {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, ] +[[package]] +name = "myst-parser" +version = "0.18.1" +description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "myst-parser-0.18.1.tar.gz", hash = "sha256:79317f4bb2c13053dd6e64f9da1ba1da6cd9c40c8a430c447a7b146a594c246d"}, + {file = "myst_parser-0.18.1-py3-none-any.whl", hash = "sha256:61b275b85d9f58aa327f370913ae1bec26ebad372cc99f3ab85c8ec3ee8d9fb8"}, +] + +[package.dependencies] +docutils = ">=0.15,<0.20" +jinja2 = "*" +markdown-it-py = ">=1.0.0,<3.0.0" +mdit-py-plugins = ">=0.3.1,<0.4.0" +pyyaml = "*" +sphinx = ">=4,<6" +typing-extensions = "*" + +[package.extras] +code-style = ["pre-commit (>=2.12,<3.0)"] +linkify = ["linkify-it-py (>=1.0,<2.0)"] +rtd = ["ipython", "sphinx-book-theme", "sphinx-design", "sphinxcontrib.mermaid (>=0.7.1,<0.8.0)", "sphinxext-opengraph (>=0.6.3,<0.7.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] +testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx (<5.2)", "sphinx-pytest"] + [[package]] name = "nodeenv" version = "1.7.0" @@ -2063,8 +2096,8 @@ files = [ name = "pockets" version = "0.9.1" description = "A collection of helpful Python tools!" -category = "main" -optional = false +category = "dev" +optional = true python-versions = "*" files = [ {file = "pockets-0.9.1-py2.py3-none-any.whl", hash = "sha256:68597934193c08a08eb2bf6a1d85593f627c22f9b065cc727a4f03f669d96d86"}, @@ -2890,6 +2923,28 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "revitron_sphinx_theme" +version = "0.7.2" +description = "" +category = "dev" +optional = false +python-versions = "*" +files = [] +develop = false + +[package.dependencies] +sphinx = "*" + +[package.extras] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] + +[package.source] +type = "git" +url = "https://github.com/revitron/revitron-sphinx-theme.git" +reference = "master" +resolved_reference = "c0779c66365d9d258d93575ebaff7db9d3aee282" + [[package]] name = "rsa" version = "4.9" @@ -3018,18 +3073,6 @@ files = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] -[[package]] -name = "soupsieve" -version = "2.3.2.post1" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, - {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, -] - [[package]] name = "speedcopy" version = "2.1.4" @@ -3044,27 +3087,27 @@ files = [ [[package]] name = "sphinx" -version = "6.1.3" +version = "5.3.0" description = "Python documentation generator" category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.6" files = [ - {file = "Sphinx-6.1.3.tar.gz", hash = "sha256:0dac3b698538ffef41716cf97ba26c1c7788dba73ce6f150c1ff5b4720786dd2"}, - {file = "sphinx-6.1.3-py3-none-any.whl", hash = "sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc"}, + {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, + {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, ] [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18,<0.20" +docutils = ">=0.14,<0.20" imagesize = ">=1.3" importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" -Pygments = ">=2.13" -requests = ">=2.25.0" +Pygments = ">=2.12" +requests = ">=2.5.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" @@ -3075,15 +3118,15 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython", "html5lib", "pytest (>=4.6)"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-autoapi" version = "2.0.1" description = "Sphinx API documentation generator" -category = "main" -optional = false +category = "dev" +optional = true python-versions = ">=3.7" files = [ {file = "sphinx-autoapi-2.0.1.tar.gz", hash = "sha256:cdf47968c20852f4feb0ccefd09e414bb820af8af8f82fab15a24b09a3d1baba"}, @@ -3102,24 +3145,6 @@ docs = ["sphinx", "sphinx-rtd-theme"] dotnet = ["sphinxcontrib-dotnetdomain"] go = ["sphinxcontrib-golangdomain"] -[[package]] -name = "sphinx-basic-ng" -version = "1.0.0b1" -description = "A modern skeleton for Sphinx themes." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx_basic_ng-1.0.0b1-py3-none-any.whl", hash = "sha256:ade597a3029c7865b24ad0eda88318766bcc2f9f4cef60df7e28126fde94db2a"}, - {file = "sphinx_basic_ng-1.0.0b1.tar.gz", hash = "sha256:89374bd3ccd9452a301786781e28c8718e99960f2d4f411845ea75fc7bb5a9b0"}, -] - -[package.dependencies] -sphinx = ">=4.0" - -[package.extras] -docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] - [[package]] name = "sphinxcontrib-applehelp" version = "1.0.3" @@ -3187,8 +3212,8 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-napoleon" version = "0.7" description = "Sphinx \"napoleon\" extension." -category = "main" -optional = false +category = "dev" +optional = true python-versions = "*" files = [ {file = "sphinxcontrib-napoleon-0.7.tar.gz", hash = "sha256:407382beed396e9f2d7f3043fad6afda95719204a1e1a231ac865f40abcbfcf8"}, @@ -3311,8 +3336,8 @@ files = [ name = "unidecode" version = "1.3.6" description = "ASCII transliterations of Unicode text" -category = "main" -optional = false +category = "dev" +optional = true python-versions = ">=3.5" files = [ {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, @@ -3611,9 +3636,9 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] -docs = ["sphinxcontrib-napoleon"] +docs = [] [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "e3b125855e7101db8c9d25fe8d42e45430c3ab1c80e9511c01b72967ba72e6bc" +content-hash = "2ec929512a99e5e7a16f46bc255171beabc41bba84e448e5312d7cd508a80de7" diff --git a/pyproject.toml b/pyproject.toml index d4b6bdbde0..cf86fb3b9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" opencolorio = "^2.2.0" +myst-parser = "^0.18.1" [tool.poetry.dev-dependencies] flake8 = "^6.0" @@ -87,11 +88,11 @@ pylint = "^2.4.4" pytest = "^6.1" pytest-cov = "*" pytest-print = "*" -Sphinx = { version = "^6.1", optional = true } +Sphinx = { version = "^5.3", optional = true } m2r2 = { version = "^0.3.3.post2", optional = true } sphinx-autoapi = { version = "^2.0.1", optional = true } sphinxcontrib-napoleon = { version = "^0.7", optional = true } -furo = { version = "^2022.12.7", optional = true } +revitron-sphinx-theme = { git = "https://github.com/revitron/revitron-sphinx-theme.git", branch = "master" } recommonmark = "*" wheel = "*" enlighten = "*" # cool terminal progress bars From fd75d7c13fd9db347a8d83e14366ea8c6113fd1a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 2 Feb 2023 15:13:06 +0100 Subject: [PATCH 199/446] :bug: fix markdown parsing --- docs/source/_static/AYON_tight_G.svg | 38 +++++++++++++++++++ docs/source/conf.py | 14 ++++--- docs/source/index.rst | 7 ++-- docs/source/modules.rst | 8 ---- docs/source/readme.rst | 6 ++- poetry.lock | 55 +++++++++++++++++++++++----- pyproject.toml | 7 ++-- 7 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 docs/source/_static/AYON_tight_G.svg delete mode 100644 docs/source/modules.rst diff --git a/docs/source/_static/AYON_tight_G.svg b/docs/source/_static/AYON_tight_G.svg new file mode 100644 index 0000000000..2c5b73deea --- /dev/null +++ b/docs/source/_static/AYON_tight_G.svg @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/docs/source/conf.py b/docs/source/conf.py index b4fe161c2b..4d20fbb3d1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -63,7 +63,6 @@ extensions = [ 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', 'sphinx.ext.autosummary', 'revitron_sphinx_theme', 'autoapi.extension', @@ -100,7 +99,7 @@ autoapi_template_dir = '_templates/autoapi' # Add any paths that contain templates here, relative to this directory. -templates_path = ['templates'] +templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: @@ -149,10 +148,15 @@ html_theme = 'revitron_sphinx_theme' # documentation. # html_theme_options = { - 'collapse_navigation': False, - 'navigation_depth': 5, - 'titles_only': False + 'collapse_navigation': True, + 'sticky_navigation': True, + 'navigation_depth': 4, + 'includehidden': True, + 'titles_only': False, + 'github_url': '', } +html_logo = '_static/AYON_tight_G.svg' + # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/source/index.rst b/docs/source/index.rst index b1ce5f2331..f703468fca 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,11 +4,12 @@ contain the root `toctree` directive. Welcome to OpenPype's API documentation! -================================ +======================================== .. toctree:: - readme - openpype + + Readme + Indices and tables ================== diff --git a/docs/source/modules.rst b/docs/source/modules.rst deleted file mode 100644 index a232a7c901..0000000000 --- a/docs/source/modules.rst +++ /dev/null @@ -1,8 +0,0 @@ -igniter -======= - -.. toctree:: - :maxdepth: 6 - - igniter - openpype diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 842932f494..138b88bba8 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -1,2 +1,6 @@ -.. title:: OpenPype Readme +=============== +OpenPype Readme +=============== + .. include:: ../../README.md + :parser: myst_parser.sphinx_ diff --git a/poetry.lock b/poetry.lock index 4107ea8335..d7bdc5f7c4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1487,6 +1487,27 @@ files = [ {file = "lief-0.12.3.zip", hash = "sha256:62e81d2f1a827d43152aed12446a604627e8833493a51dca027026eed0ce7128"}, ] +[[package]] +name = "linkify-it-py" +version = "2.0.0" +description = "Links recognition library with FULL unicode support." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "linkify-it-py-2.0.0.tar.gz", hash = "sha256:476464480906bed8b2fa3813bf55566282e55214ad7e41b7d1c2b564666caf2f"}, + {file = "linkify_it_py-2.0.0-py3-none-any.whl", hash = "sha256:1bff43823e24e507a099e328fc54696124423dd6320c75a9da45b4b754b748ad"}, +] + +[package.dependencies] +uc-micro-py = "*" + +[package.extras] +benchmark = ["pytest", "pytest-benchmark"] +dev = ["black", "flake8", "isort", "pre-commit"] +doc = ["myst-parser", "sphinx", "sphinx-book-theme"] +test = ["coverage", "pytest", "pytest-cov"] + [[package]] name = "log4mongo" version = "1.7.0" @@ -2250,20 +2271,21 @@ files = [ [[package]] name = "pydocstyle" -version = "3.0.0" +version = "6.3.0" description = "Python docstring style checker" -category = "dev" +category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "pydocstyle-3.0.0-py2-none-any.whl", hash = "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8"}, - {file = "pydocstyle-3.0.0-py3-none-any.whl", hash = "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039"}, - {file = "pydocstyle-3.0.0.tar.gz", hash = "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4"}, + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, ] [package.dependencies] -six = "*" -snowballstemmer = "*" +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] [[package]] name = "pyflakes" @@ -3332,6 +3354,21 @@ files = [ {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] +[[package]] +name = "uc-micro-py" +version = "1.0.1" +description = "Micro subset of unicode data files for linkify-it-py projects." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "uc-micro-py-1.0.1.tar.gz", hash = "sha256:b7cdf4ea79433043ddfe2c82210208f26f7962c0cfbe3bacb05ee879a7fdb596"}, + {file = "uc_micro_py-1.0.1-py3-none-any.whl", hash = "sha256:316cfb8b6862a0f1d03540f0ae6e7b033ff1fa0ddbe60c12cbe0d4cec846a69f"}, +] + +[package.extras] +test = ["coverage", "pytest", "pytest-cov"] + [[package]] name = "unidecode" version = "1.3.6" @@ -3641,4 +3678,4 @@ docs = [] [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "2ec929512a99e5e7a16f46bc255171beabc41bba84e448e5312d7cd508a80de7" +content-hash = "47518c544a90cdb3e99e83533557515d0d47079ac4461708ce71ab3ce97b9987" diff --git a/pyproject.toml b/pyproject.toml index cf86fb3b9f..b2b0e4c071 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" opencolorio = "^2.2.0" -myst-parser = "^0.18.1" [tool.poetry.dev-dependencies] flake8 = "^6.0" @@ -82,8 +81,10 @@ GitPython = "^3.1.17" jedi = "^0.13" Jinja2 = "^3" markupsafe = "2.0.1" -pycodestyle = "^2.5.0" -pydocstyle = "^3.0.0" +pycodestyle = "*" +pydocstyle = "*" +linkify-it-py = "^2.0.0" +myst-parser = "^0.18.1" pylint = "^2.4.4" pytest = "^6.1" pytest-cov = "*" From 74e4b3d4f2d16e5aa99ff05bde21be927e899e4e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 3 Feb 2023 17:59:41 +0100 Subject: [PATCH 200/446] :bug: make autoapi mandatory --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b2b0e4c071..fe9c228ea9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,10 +89,10 @@ pylint = "^2.4.4" pytest = "^6.1" pytest-cov = "*" pytest-print = "*" -Sphinx = { version = "^5.3", optional = true } -m2r2 = { version = "^0.3.3.post2", optional = true } -sphinx-autoapi = { version = "^2.0.1", optional = true } -sphinxcontrib-napoleon = { version = "^0.7", optional = true } +Sphinx = "^5.3" +m2r2 = "^0.3.3.post2" +sphinx-autoapi = "^2.0.1" +sphinxcontrib-napoleon = "^0.7" revitron-sphinx-theme = { git = "https://github.com/revitron/revitron-sphinx-theme.git", branch = "master" } recommonmark = "*" wheel = "*" From 2fdb0ece6b43503b490beb2a83bb40a74e0883f0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 3 Mar 2023 17:18:50 +0100 Subject: [PATCH 201/446] :memo: add documentation about building documentation --- docs/README.md | 74 +++++++++++++++++++++++++++++++++++++++++++++ docs/source/conf.py | 5 --- 2 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..102da990aa --- /dev/null +++ b/docs/README.md @@ -0,0 +1,74 @@ +API Documentation +================= + +This documents the way how to build and modify API documentation using Sphinx and AutoAPI. Ground for documentation +should be directly in sources - in docstrings and markdowns. Sphinx and AutoAPI will crawl over them and generate +RST files that are in turn used to generate HTML documentation. For docstrings we prefer "Napoleon" or "Google" style +docstrings, but RST is also acceptable mainly in cases where you need to use Sphinx directives. + +Using only docstrings is not really viable as some documentation should be done on higher level - like overview of +some modules/functionality and so on. This should be done directly in RST files and committed to repository. + +Configuration +------------- +Configuration is done in `/docs/source/conf.py`. The most important settings are: + +- `autodoc_mock_imports`: add modules that can't be actually imported by Sphinx in running environment, like `nuke`, `maya`, etc. +- `autoapi_ignore`: add directories that shouldn't be processed by **AutoAPI**, like vendor dirs, etc. +- `html_theme_options`: you can use these options to influence how the html theme of the generated files will look. +- `myst_gfm_only`: are Myst parser option for Markdown setting what flavour of Markdown should be used. + +How to build it +--------------- + +You can run: + +```sh +cd .\docs +make.bat html +``` + +on linux/macOS: + +```sh +cd ./docs +make html +``` + +This will go over our code and generate **.rst** files in `/docs/source/autoapi` and from those it will generate +full html documentation in `/docs/build/html`. + +During the build you may see tons of red errors that are pointing to our issues: + +1) **Wrong imports** - +Invalid import are usually wrong relative imports (too deep) or circular imports. +2) **Invalid docstrings** - +Docstrings to be processed into documentation needs to follow some syntax - this can be checked by running +`pydocstyle` that is already included with OpenPype +3) **Invalid markdown/rst files** - +Markdown/RST files can be included inside RST files using `.. include::` directive. But they have to be properly +formatted. + +Editing RST templates +--------------------- +Everything starts with `/docs/source/index.rst` - this file should be properly edited, Right now it just +includes `readme.rst` that in turn include and parse main `README.md`. This is entrypoint to API documentation. +All templates generated by AutoAPI are in `/docs/source/autoapi`. They should be eventually committed to repository +and edited too. + +Steps for enhancing API documentation +------------------------------------- + +1) Run `/docs/make.bat html` +2) Read the red errors/warnings - fix it in the code +3) Run `/docs/make.bat html` - again until there are no red lines +4) Edit RST files and add some meaningful content there + +Resources +========= + +- [ReStructuredText on Wikipedia](https://en.wikipedia.org/wiki/ReStructuredText) +- [RST Quick Reference](https://docutils.sourceforge.io/docs/user/rst/quickref.html) +- [Sphinx AutoAPI Documentation](https://sphinx-autoapi.readthedocs.io/en/latest/) +- [Example of Google Style Python Docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) +- [Sphinx Directives](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html) diff --git a/docs/source/conf.py b/docs/source/conf.py index 4d20fbb3d1..916a397e8e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -258,9 +258,4 @@ intersphinx_mapping = { 'https://docs.python.org/3/': None } -# -- Options for todo extension ---------------------------------------------- - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - myst_gfm_only = True From f58994d59c970878c8d81cb1a1ec19b6748c482a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 17 Mar 2023 17:25:19 +0100 Subject: [PATCH 202/446] Loader: Remove `context` argument from Loader.__init__() (#4602) * Remove Loader `context` argument to __init__ * Add backwards compatibility for Loader.load by still setting `.fname` attr * Refactor/remove usage of `self.fname` in loaders * Fix some refactoring * Fix some refactoring * Hound * Revert invalid refactor * Fix refactor * Fix playblast panel collection * Refactor missing method * Fix typo * Use the correct `context` --------- Co-authored-by: Toke Stuart Jepsen Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> Co-authored-by: Jakub Trllo --- .../plugins/load/load_background.py | 5 ++-- .../aftereffects/plugins/load/load_file.py | 16 ++++++------- openpype/hosts/blender/api/plugin.py | 3 ++- .../blender/plugins/load/import_workfile.py | 6 +++-- .../hosts/blender/plugins/load/load_abc.py | 2 +- .../hosts/blender/plugins/load/load_action.py | 2 +- .../blender/plugins/load/load_animation.py | 2 +- .../hosts/blender/plugins/load/load_audio.py | 2 +- .../blender/plugins/load/load_camera_blend.py | 2 +- .../blender/plugins/load/load_camera_fbx.py | 2 +- .../hosts/blender/plugins/load/load_fbx.py | 2 +- .../blender/plugins/load/load_layout_blend.py | 2 +- .../blender/plugins/load/load_layout_json.py | 2 +- .../hosts/blender/plugins/load/load_look.py | 2 +- .../hosts/blender/plugins/load/load_model.py | 2 +- .../hosts/blender/plugins/load/load_rig.py | 2 +- .../hosts/flame/plugins/load/load_clip.py | 3 ++- .../flame/plugins/load/load_clip_batch.py | 5 ++-- .../hosts/fusion/plugins/load/load_alembic.py | 2 +- .../hosts/fusion/plugins/load/load_fbx.py | 2 +- .../fusion/plugins/load/load_sequence.py | 9 +++----- .../harmony/plugins/load/load_background.py | 5 ++-- .../plugins/load/load_imagesequence.py | 2 +- openpype/hosts/hiero/api/plugin.py | 6 +++-- .../hosts/hiero/plugins/load/load_clip.py | 3 ++- .../hosts/hiero/plugins/load/load_effects.py | 3 ++- .../houdini/plugins/load/load_alembic.py | 3 ++- .../plugins/load/load_alembic_archive.py | 3 ++- .../hosts/houdini/plugins/load/load_bgeo.py | 3 ++- .../hosts/houdini/plugins/load/load_camera.py | 3 ++- .../hosts/houdini/plugins/load/load_hda.py | 3 ++- .../hosts/houdini/plugins/load/load_image.py | 3 ++- .../houdini/plugins/load/load_usd_layer.py | 3 ++- .../plugins/load/load_usd_reference.py | 3 ++- .../hosts/houdini/plugins/load/load_vdb.py | 3 ++- .../houdini/plugins/load/show_usdview.py | 3 ++- .../hosts/max/plugins/load/load_camera_fbx.py | 3 ++- .../hosts/max/plugins/load/load_max_scene.py | 4 +++- .../hosts/max/plugins/load/load_pointcache.py | 3 ++- openpype/hosts/maya/api/plugin.py | 3 ++- .../maya/plugins/load/_load_animation.py | 3 ++- openpype/hosts/maya/plugins/load/actions.py | 3 ++- .../maya/plugins/load/load_arnold_standin.py | 5 ++-- .../hosts/maya/plugins/load/load_assembly.py | 2 +- .../hosts/maya/plugins/load/load_gpucache.py | 3 ++- openpype/hosts/maya/plugins/load/load_look.py | 6 +++-- .../hosts/maya/plugins/load/load_matchmove.py | 10 ++++---- .../maya/plugins/load/load_multiverse_usd.py | 4 +++- .../plugins/load/load_multiverse_usd_over.py | 3 ++- .../maya/plugins/load/load_redshift_proxy.py | 4 ++-- .../hosts/maya/plugins/load/load_reference.py | 3 ++- .../maya/plugins/load/load_rendersetup.py | 5 ++-- .../maya/plugins/load/load_vdb_to_arnold.py | 3 ++- .../maya/plugins/load/load_vdb_to_redshift.py | 2 +- .../maya/plugins/load/load_vdb_to_vray.py | 7 +++--- .../hosts/maya/plugins/load/load_vrayproxy.py | 8 ++++--- .../hosts/maya/plugins/load/load_vrayscene.py | 6 +++-- openpype/hosts/maya/plugins/load/load_xgen.py | 3 ++- .../maya/plugins/load/load_yeti_cache.py | 3 ++- .../hosts/maya/plugins/load/load_yeti_rig.py | 3 ++- .../hosts/nuke/plugins/load/load_backdrop.py | 2 +- .../nuke/plugins/load/load_camera_abc.py | 2 +- openpype/hosts/nuke/plugins/load/load_clip.py | 12 +++++----- .../hosts/nuke/plugins/load/load_effects.py | 2 +- .../nuke/plugins/load/load_effects_ip.py | 2 +- .../hosts/nuke/plugins/load/load_gizmo.py | 2 +- .../hosts/nuke/plugins/load/load_gizmo_ip.py | 2 +- .../hosts/nuke/plugins/load/load_image.py | 2 +- .../hosts/nuke/plugins/load/load_matchmove.py | 5 ++-- .../hosts/nuke/plugins/load/load_model.py | 2 +- .../nuke/plugins/load/load_script_precomp.py | 4 ++-- openpype/hosts/photoshop/api/README.md | 3 ++- .../photoshop/plugins/load/load_image.py | 3 ++- .../plugins/load/load_image_from_sequence.py | 10 ++++---- .../photoshop/plugins/load/load_reference.py | 3 ++- openpype/hosts/resolve/api/plugin.py | 3 ++- .../hosts/resolve/plugins/load/load_clip.py | 7 +++--- .../hosts/tvpaint/plugins/load/load_image.py | 3 ++- .../plugins/load/load_reference_image.py | 7 +++--- .../hosts/tvpaint/plugins/load/load_sound.py | 3 ++- .../tvpaint/plugins/load/load_workfile.py | 20 ++++++++-------- .../plugins/load/load_alembic_animation.py | 3 ++- .../unreal/plugins/load/load_animation.py | 9 ++++---- .../hosts/unreal/plugins/load/load_camera.py | 3 ++- .../plugins/load/load_geometrycache_abc.py | 3 ++- .../hosts/unreal/plugins/load/load_layout.py | 3 ++- .../plugins/load/load_layout_existing.py | 3 ++- .../plugins/load/load_skeletalmesh_abc.py | 3 ++- .../plugins/load/load_skeletalmesh_fbx.py | 5 ++-- .../plugins/load/load_staticmesh_abc.py | 3 ++- .../plugins/load/load_staticmesh_fbx.py | 3 ++- .../hosts/unreal/plugins/load/load_uasset.py | 3 ++- openpype/pipeline/load/plugins.py | 6 ----- openpype/pipeline/load/utils.py | 23 ++++++++++--------- openpype/plugins/load/copy_file.py | 5 ++-- openpype/plugins/load/copy_file_path.py | 5 ++-- openpype/plugins/load/open_djv.py | 6 +++-- openpype/plugins/load/open_file.py | 2 +- website/docs/dev_colorspace.md | 2 +- 99 files changed, 237 insertions(+), 175 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/load/load_background.py b/openpype/hosts/aftereffects/plugins/load/load_background.py index e7c29fee5a..16f45074aa 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_background.py +++ b/openpype/hosts/aftereffects/plugins/load/load_background.py @@ -33,9 +33,10 @@ class BackgroundLoader(api.AfterEffectsLoader): existing_items, "{}_{}".format(context["asset"]["name"], name)) - layers = get_background_layers(self.fname) + path = self.filepath_from_context(context) + layers = get_background_layers(path) if not layers: - raise ValueError("No layers found in {}".format(self.fname)) + raise ValueError("No layers found in {}".format(path)) comp = stub.import_background(None, stub.LOADED_ICON + comp_name, layers) diff --git a/openpype/hosts/aftereffects/plugins/load/load_file.py b/openpype/hosts/aftereffects/plugins/load/load_file.py index 33a86aa505..def7c927ab 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_file.py +++ b/openpype/hosts/aftereffects/plugins/load/load_file.py @@ -29,32 +29,32 @@ class FileLoader(api.AfterEffectsLoader): import_options = {} - file = self.fname + path = self.filepath_from_context(context) repr_cont = context["representation"]["context"] - if "#" not in file: + if "#" not in path: frame = repr_cont.get("frame") if frame: padding = len(frame) - file = file.replace(frame, "#" * padding) + path = path.replace(frame, "#" * padding) import_options['sequence'] = True - if not file: + if not path: repr_id = context["representation"]["_id"] self.log.warning( "Representation id `{}` is failing to load".format(repr_id)) return - file = file.replace("\\", "/") - if '.psd' in file: + path = path.replace("\\", "/") + if '.psd' in path: import_options['ImportAsType'] = 'ImportAsType.COMP' - comp = stub.import_file(self.fname, stub.LOADED_ICON + comp_name, + comp = stub.import_file(path, stub.LOADED_ICON + comp_name, import_options) if not comp: self.log.warning( - "Representation id `{}` is failing to load".format(file)) + "Representation `{}` is failing to load".format(path)) self.log.warning("Check host app for alert error.") return diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 1274795c6b..fb87d08cce 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -243,7 +243,8 @@ class AssetLoader(LoaderPlugin): """ # TODO (jasper): make it possible to add the asset several times by # just re-using the collection - assert Path(self.fname).exists(), f"{self.fname} doesn't exist." + filepath = self.filepath_from_context(context) + assert Path(filepath).exists(), f"{filepath} doesn't exist." asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/blender/plugins/load/import_workfile.py b/openpype/hosts/blender/plugins/load/import_workfile.py index bbdf1c7ea0..4f5016d422 100644 --- a/openpype/hosts/blender/plugins/load/import_workfile.py +++ b/openpype/hosts/blender/plugins/load/import_workfile.py @@ -52,7 +52,8 @@ class AppendBlendLoader(plugin.AssetLoader): color = "#775555" def load(self, context, name=None, namespace=None, data=None): - append_workfile(context, self.fname, False) + path = self.filepath_from_context(context) + append_workfile(context, path, False) # We do not containerize imported content, it remains unmanaged return @@ -76,7 +77,8 @@ class ImportBlendLoader(plugin.AssetLoader): color = "#775555" def load(self, context, name=None, namespace=None, data=None): - append_workfile(context, self.fname, True) + path = self.filepath_from_context(context) + append_workfile(context, path, True) # We do not containerize imported content, it remains unmanaged return diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index c1d73eff02..292925c833 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -111,7 +111,7 @@ class CacheModelLoader(plugin.AssetLoader): options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/blender/plugins/load/load_action.py b/openpype/hosts/blender/plugins/load/load_action.py index 3c8fe988f0..3447e67ebf 100644 --- a/openpype/hosts/blender/plugins/load/load_action.py +++ b/openpype/hosts/blender/plugins/load/load_action.py @@ -43,7 +43,7 @@ class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader): options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] lib_container = openpype.hosts.blender.api.plugin.asset_name(asset, subset) diff --git a/openpype/hosts/blender/plugins/load/load_animation.py b/openpype/hosts/blender/plugins/load/load_animation.py index 6b8d4abd04..3e7f808903 100644 --- a/openpype/hosts/blender/plugins/load/load_animation.py +++ b/openpype/hosts/blender/plugins/load/load_animation.py @@ -34,7 +34,7 @@ class BlendAnimationLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) with bpy.data.libraries.load( libpath, link=True, relative=False diff --git a/openpype/hosts/blender/plugins/load/load_audio.py b/openpype/hosts/blender/plugins/load/load_audio.py index 3f4fcc17de..ac8f363316 100644 --- a/openpype/hosts/blender/plugins/load/load_audio.py +++ b/openpype/hosts/blender/plugins/load/load_audio.py @@ -38,7 +38,7 @@ class AudioLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/blender/plugins/load/load_camera_blend.py b/openpype/hosts/blender/plugins/load/load_camera_blend.py index f00027f0b4..bd4820bf78 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_blend.py +++ b/openpype/hosts/blender/plugins/load/load_camera_blend.py @@ -110,7 +110,7 @@ class BlendCameraLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/blender/plugins/load/load_camera_fbx.py b/openpype/hosts/blender/plugins/load/load_camera_fbx.py index 97f844e610..b9d05dda0a 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_camera_fbx.py @@ -86,7 +86,7 @@ class FbxCameraLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/blender/plugins/load/load_fbx.py b/openpype/hosts/blender/plugins/load/load_fbx.py index ee2e7d175c..e129ea6754 100644 --- a/openpype/hosts/blender/plugins/load/load_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_fbx.py @@ -130,7 +130,7 @@ class FbxModelLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index 7d2fd23444..03ccbce3d7 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -267,7 +267,7 @@ class BlendLayoutLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] representation = str(context["representation"]["_id"]) diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index eca098627e..81683b8de8 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -144,7 +144,7 @@ class JsonLayoutLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/blender/plugins/load/load_look.py b/openpype/hosts/blender/plugins/load/load_look.py index 70d1b95f02..c121f55633 100644 --- a/openpype/hosts/blender/plugins/load/load_look.py +++ b/openpype/hosts/blender/plugins/load/load_look.py @@ -92,7 +92,7 @@ class BlendLookLoader(plugin.AssetLoader): options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index 0a5d98ffa0..3210a4e841 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -113,7 +113,7 @@ class BlendModelLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index 1d23a70061..b9b5ad935f 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -181,7 +181,7 @@ class BlendRigLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/flame/plugins/load/load_clip.py b/openpype/hosts/flame/plugins/load/load_clip.py index dfb2d2b6f0..338833b449 100644 --- a/openpype/hosts/flame/plugins/load/load_clip.py +++ b/openpype/hosts/flame/plugins/load/load_clip.py @@ -82,8 +82,9 @@ class LoadClip(opfapi.ClipLoader): os.makedirs(openclip_dir) # prepare clip data from context ad send it to openClipLoader + path = self.filepath_from_context(context) loading_context = { - "path": self.fname.replace("\\", "/"), + "path": path.replace("\\", "/"), "colorspace": colorspace, "version": "v{:0>3}".format(version_name), "layer_rename_template": self.layer_rename_template, diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 5c5a77f0d0..ca43b94ee9 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -81,9 +81,10 @@ class LoadClipBatch(opfapi.ClipLoader): if not os.path.exists(openclip_dir): os.makedirs(openclip_dir) - # prepare clip data from context ad send it to openClipLoader + # prepare clip data from context and send it to openClipLoader + path = self.filepath_from_context(context) loading_context = { - "path": self.fname.replace("\\", "/"), + "path": path.replace("\\", "/"), "colorspace": colorspace, "version": "v{:0>3}".format(version_name), "layer_rename_template": self.layer_rename_template, diff --git a/openpype/hosts/fusion/plugins/load/load_alembic.py b/openpype/hosts/fusion/plugins/load/load_alembic.py index 11bf59af12..9b6d1e12b4 100644 --- a/openpype/hosts/fusion/plugins/load/load_alembic.py +++ b/openpype/hosts/fusion/plugins/load/load_alembic.py @@ -32,7 +32,7 @@ class FusionLoadAlembicMesh(load.LoaderPlugin): comp = get_current_comp() with comp_lock_and_undo_chunk(comp, "Create tool"): - path = self.fname + path = self.filepath_from_context(context) args = (-32768, -32768) tool = comp.AddTool(self.tool_type, *args) diff --git a/openpype/hosts/fusion/plugins/load/load_fbx.py b/openpype/hosts/fusion/plugins/load/load_fbx.py index c73ad78394..d15d2c33d7 100644 --- a/openpype/hosts/fusion/plugins/load/load_fbx.py +++ b/openpype/hosts/fusion/plugins/load/load_fbx.py @@ -45,7 +45,7 @@ class FusionLoadFBXMesh(load.LoaderPlugin): # Create the Loader with the filename path set comp = get_current_comp() with comp_lock_and_undo_chunk(comp, "Create tool"): - path = self.fname + path = self.filepath_from_context(context) args = (-32768, -32768) tool = comp.AddTool(self.tool_type, *args) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 552e282587..20be5faaba 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -1,10 +1,7 @@ import contextlib import openpype.pipeline.load as load -from openpype.pipeline.load import ( - get_representation_context, - get_representation_path_from_context, -) +from openpype.pipeline.load import get_representation_context from openpype.hosts.fusion.api import ( imprint_container, get_current_comp, @@ -157,7 +154,7 @@ class FusionLoadSequence(load.LoaderPlugin): namespace = context["asset"]["name"] # Use the first file for now - path = get_representation_path_from_context(context) + path = self.filepath_from_context(context) # Create the Loader with the filename path set comp = get_current_comp() @@ -228,7 +225,7 @@ class FusionLoadSequence(load.LoaderPlugin): comp = tool.Comp() context = get_representation_context(representation) - path = get_representation_path_from_context(context) + path = self.filepath_from_context(context) # Get start frame from version data start = self._get_start(context["version"], tool) diff --git a/openpype/hosts/harmony/plugins/load/load_background.py b/openpype/hosts/harmony/plugins/load/load_background.py index c28a87791e..853d347c2e 100644 --- a/openpype/hosts/harmony/plugins/load/load_background.py +++ b/openpype/hosts/harmony/plugins/load/load_background.py @@ -238,7 +238,8 @@ class BackgroundLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): - with open(self.fname) as json_file: + path = self.filepath_from_context(context) + with open(path) as json_file: data = json.load(json_file) layers = list() @@ -251,7 +252,7 @@ class BackgroundLoader(load.LoaderPlugin): if layer.get("filename"): layers.append(layer["filename"]) - bg_folder = os.path.dirname(self.fname) + bg_folder = os.path.dirname(path) subset_name = context["subset"]["name"] # read_node_name += "_{}".format(uuid.uuid4()) diff --git a/openpype/hosts/harmony/plugins/load/load_imagesequence.py b/openpype/hosts/harmony/plugins/load/load_imagesequence.py index b95d25f507..754f82e5d5 100644 --- a/openpype/hosts/harmony/plugins/load/load_imagesequence.py +++ b/openpype/hosts/harmony/plugins/load/load_imagesequence.py @@ -34,7 +34,7 @@ class ImageSequenceLoader(load.LoaderPlugin): data (dict, optional): Additional data passed into loader. """ - fname = Path(self.fname) + fname = Path(self.filepath_from_context(context)) self_name = self.__class__.__name__ collections, remainder = clique.assemble( os.listdir(fname.parent.as_posix()) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index a3f8a6c524..65a4009756 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -12,6 +12,7 @@ from openpype.settings import get_current_project_settings from openpype.lib import Logger from openpype.pipeline import LoaderPlugin, LegacyCreator from openpype.pipeline.context_tools import get_current_project_asset +from openpype.pipeline.load import get_representation_path_from_context from . import lib log = Logger.get_logger(__name__) @@ -393,7 +394,7 @@ class ClipLoader: active_bin = None data = dict() - def __init__(self, cls, context, **options): + def __init__(self, cls, context, path, **options): """ Initialize object Arguments: @@ -406,6 +407,7 @@ class ClipLoader: self.__dict__.update(cls.__dict__) self.context = context self.active_project = lib.get_current_project() + self.fname = path # try to get value from options or evaluate key value for `handles` self.with_handles = options.get("handles") or bool( @@ -467,7 +469,7 @@ class ClipLoader: self.data["track_name"] = "_".join([subset, representation]) self.data["versionData"] = self.context["version"]["data"] # gets file path - file = self.fname + file = get_representation_path_from_context(self.context) if not file: repr_id = repr["_id"] log.warning( diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index c9bebfa8b2..71df52f0f8 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -87,7 +87,8 @@ class LoadClip(phiero.SequenceLoader): }) # load clip to timeline and get main variables - track_item = phiero.ClipLoader(self, context, **options).load() + path = self.filepath_from_context(context) + track_item = phiero.ClipLoader(self, context, path, **options).load() namespace = namespace or track_item.name() version = context['version'] version_data = version.get("data", {}) diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index b61cca9731..4b86149166 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -59,7 +59,8 @@ class LoadEffects(load.LoaderPlugin): } # getting file path - file = self.fname.replace("\\", "/") + file = self.filepath_from_context(context) + file = file.replace("\\", "/") if self._shared_loading( file, diff --git a/openpype/hosts/houdini/plugins/load/load_alembic.py b/openpype/hosts/houdini/plugins/load/load_alembic.py index c6f0ebf2f9..48bd730ebe 100644 --- a/openpype/hosts/houdini/plugins/load/load_alembic.py +++ b/openpype/hosts/houdini/plugins/load/load_alembic.py @@ -20,7 +20,8 @@ class AbcLoader(load.LoaderPlugin): import hou # Format file name, Houdini only wants forward slashes - file_path = os.path.normpath(self.fname) + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node diff --git a/openpype/hosts/houdini/plugins/load/load_alembic_archive.py b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py index 47d2e1b896..3a577f72b4 100644 --- a/openpype/hosts/houdini/plugins/load/load_alembic_archive.py +++ b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py @@ -21,7 +21,8 @@ class AbcArchiveLoader(load.LoaderPlugin): import hou # Format file name, Houdini only wants forward slashes - file_path = os.path.normpath(self.fname) + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node diff --git a/openpype/hosts/houdini/plugins/load/load_bgeo.py b/openpype/hosts/houdini/plugins/load/load_bgeo.py index 86e8675c02..22680178c0 100644 --- a/openpype/hosts/houdini/plugins/load/load_bgeo.py +++ b/openpype/hosts/houdini/plugins/load/load_bgeo.py @@ -43,9 +43,10 @@ class BgeoLoader(load.LoaderPlugin): file_node.destroy() # Explicitly create a file node + path = self.filepath_from_context(context) file_node = container.createNode("file", node_name=node_name) file_node.setParms( - {"file": self.format_path(self.fname, context["representation"])}) + {"file": self.format_path(path, context["representation"])}) # Set display on last node file_node.setDisplayFlag(True) diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index 6365508f4e..7b4a04809e 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -94,7 +94,8 @@ class CameraLoader(load.LoaderPlugin): import hou # Format file name, Houdini only wants forward slashes - file_path = os.path.normpath(self.fname) + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py index 2438570c6e..57edc341a3 100644 --- a/openpype/hosts/houdini/plugins/load/load_hda.py +++ b/openpype/hosts/houdini/plugins/load/load_hda.py @@ -21,7 +21,8 @@ class HdaLoader(load.LoaderPlugin): import hou # Format file name, Houdini only wants forward slashes - file_path = os.path.normpath(self.fname) + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py index 26bc569c53..663a93e48b 100644 --- a/openpype/hosts/houdini/plugins/load/load_image.py +++ b/openpype/hosts/houdini/plugins/load/load_image.py @@ -55,7 +55,8 @@ class ImageLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): # Format file name, Houdini only wants forward slashes - file_path = os.path.normpath(self.fname) + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") file_path = self._get_file_sequence(file_path) diff --git a/openpype/hosts/houdini/plugins/load/load_usd_layer.py b/openpype/hosts/houdini/plugins/load/load_usd_layer.py index 1f0ec25128..1528cf549f 100644 --- a/openpype/hosts/houdini/plugins/load/load_usd_layer.py +++ b/openpype/hosts/houdini/plugins/load/load_usd_layer.py @@ -26,7 +26,8 @@ class USDSublayerLoader(load.LoaderPlugin): import hou # Format file name, Houdini only wants forward slashes - file_path = os.path.normpath(self.fname) + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node diff --git a/openpype/hosts/houdini/plugins/load/load_usd_reference.py b/openpype/hosts/houdini/plugins/load/load_usd_reference.py index f66d05395e..8402ad072c 100644 --- a/openpype/hosts/houdini/plugins/load/load_usd_reference.py +++ b/openpype/hosts/houdini/plugins/load/load_usd_reference.py @@ -26,7 +26,8 @@ class USDReferenceLoader(load.LoaderPlugin): import hou # Format file name, Houdini only wants forward slashes - file_path = os.path.normpath(self.fname) + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node diff --git a/openpype/hosts/houdini/plugins/load/load_vdb.py b/openpype/hosts/houdini/plugins/load/load_vdb.py index 87900502c5..bcc4f200d3 100644 --- a/openpype/hosts/houdini/plugins/load/load_vdb.py +++ b/openpype/hosts/houdini/plugins/load/load_vdb.py @@ -40,8 +40,9 @@ class VdbLoader(load.LoaderPlugin): # Explicitly create a file node file_node = container.createNode("file", node_name=node_name) + path = self.filepath_from_context(context) file_node.setParms( - {"file": self.format_path(self.fname, context["representation"])}) + {"file": self.format_path(path, context["representation"])}) # Set display on last node file_node.setDisplayFlag(True) diff --git a/openpype/hosts/houdini/plugins/load/show_usdview.py b/openpype/hosts/houdini/plugins/load/show_usdview.py index 2737bc40fa..7b03a0738a 100644 --- a/openpype/hosts/houdini/plugins/load/show_usdview.py +++ b/openpype/hosts/houdini/plugins/load/show_usdview.py @@ -20,7 +20,8 @@ class ShowInUsdview(load.LoaderPlugin): usdview = find_executable("usdview") - filepath = os.path.normpath(self.fname) + filepath = self.filepath_from_context(context) + filepath = os.path.normpath(filepath) filepath = filepath.replace("\\", "/") if not os.path.exists(filepath): diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index c51900dbb7..62284b23d9 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -17,7 +17,8 @@ class FbxLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - filepath = os.path.normpath(self.fname) + filepath = self.filepath_from_context(context) + filepath = os.path.normpath(filepath) rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("AxisConversionMethod", True) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index e3fb34f5bc..76cd3bf367 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -19,7 +19,9 @@ class MaxSceneLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - path = os.path.normpath(self.fname) + + path = self.filepath_from_context(context) + path = os.path.normpath(path) # import the max scene by using "merge file" path = path.replace('\\', '/') rt.MergeMaxFile(path) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index cadbe7cac2..290503e053 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -23,7 +23,8 @@ class AbcLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - file_path = os.path.normpath(self.fname) + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) abc_before = { c diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 9a67147eb4..2b5aee9700 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -485,7 +485,8 @@ class ReferenceLoader(Loader): namespace=None, options=None ): - assert os.path.exists(self.fname), "%s does not exist." % self.fname + path = self.filepath_from_context(context) + assert os.path.exists(path), "%s does not exist." % path asset = context['asset'] subset = context['subset'] diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index 2ba5fe6b64..49792b2806 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -35,7 +35,8 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # hero_001 (abc) # asset_counter{optional} - file_url = self.prepare_root_value(self.fname, + path = self.filepath_from_context(context) + file_url = self.prepare_root_value(path, context["project"]["name"]) nodes = cmds.file(file_url, namespace=namespace, diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index 4855f3eed0..348657e592 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -138,8 +138,9 @@ class ImportMayaLoader(load.LoaderPlugin): suffix="_", ) + path = self.filepath_from_context(context) with maintained_selection(): - nodes = cmds.file(self.fname, + nodes = cmds.file(path, i=True, preserveReferences=True, namespace=namespace, diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 29215bc5c2..b5cc4d629b 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -89,11 +89,12 @@ class ArnoldStandinLoader(load.LoaderPlugin): cmds.parent(standin, root) # Set the standin filepath + repre_path = self.filepath_from_context(context) path, operator = self._setup_proxy( - standin_shape, self.fname, namespace + standin_shape, repre_path, namespace ) cmds.setAttr(standin_shape + ".dso", path, type="string") - sequence = is_sequence(os.listdir(os.path.dirname(self.fname))) + sequence = is_sequence(os.listdir(os.path.dirname(repre_path))) cmds.setAttr(standin_shape + ".useFrameExtension", sequence) fps = float(version["data"].get("fps"))or get_current_session_fps() diff --git a/openpype/hosts/maya/plugins/load/load_assembly.py b/openpype/hosts/maya/plugins/load/load_assembly.py index 275f21be5d..0a2733e03c 100644 --- a/openpype/hosts/maya/plugins/load/load_assembly.py +++ b/openpype/hosts/maya/plugins/load/load_assembly.py @@ -30,7 +30,7 @@ class AssemblyLoader(load.LoaderPlugin): ) containers = setdress.load_package( - filepath=self.fname, + filepath=self.filepath_from_context(context), name=name, namespace=namespace ) diff --git a/openpype/hosts/maya/plugins/load/load_gpucache.py b/openpype/hosts/maya/plugins/load/load_gpucache.py index 794b21eb5d..52be1ca645 100644 --- a/openpype/hosts/maya/plugins/load/load_gpucache.py +++ b/openpype/hosts/maya/plugins/load/load_gpucache.py @@ -56,7 +56,8 @@ class GpuCacheLoader(load.LoaderPlugin): name="{0}Shape".format(transform_name)) # Set the cache filepath - cmds.setAttr(cache + '.cacheFileName', self.fname, type="string") + path = self.filepath_from_context(context) + cmds.setAttr(cache + '.cacheFileName', path, type="string") cmds.setAttr(cache + '.cacheGeomPath', "|", type="string") # root # Lock parenting of the transform and cache diff --git a/openpype/hosts/maya/plugins/load/load_look.py b/openpype/hosts/maya/plugins/load/load_look.py index b060ae2b05..3cc87b7ef4 100644 --- a/openpype/hosts/maya/plugins/load/load_look.py +++ b/openpype/hosts/maya/plugins/load/load_look.py @@ -32,8 +32,10 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): from maya import cmds with lib.maintained_selection(): - file_url = self.prepare_root_value(self.fname, - context["project"]["name"]) + file_url = self.prepare_root_value( + file_url=self.filepath_from_context(context), + project_name=context["project"]["name"] + ) nodes = cmds.file(file_url, namespace=namespace, reference=True, diff --git a/openpype/hosts/maya/plugins/load/load_matchmove.py b/openpype/hosts/maya/plugins/load/load_matchmove.py index ee3332bd09..46d1be8300 100644 --- a/openpype/hosts/maya/plugins/load/load_matchmove.py +++ b/openpype/hosts/maya/plugins/load/load_matchmove.py @@ -1,7 +1,6 @@ from maya import mel from openpype.pipeline import load - class MatchmoveLoader(load.LoaderPlugin): """ This will run matchmove script to create track in scene. @@ -18,11 +17,12 @@ class MatchmoveLoader(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, data): - if self.fname.lower().endswith(".py"): - exec(open(self.fname).read()) + path = self.filepath_from_context(context) + if path.lower().endswith(".py"): + exec(open(path).read()) - elif self.fname.lower().endswith(".mel"): - mel.eval('source "{}"'.format(self.fname)) + elif path.lower().endswith(".mel"): + mel.eval('source "{}"'.format(path)) else: self.log.error("Unsupported script type") diff --git a/openpype/hosts/maya/plugins/load/load_multiverse_usd.py b/openpype/hosts/maya/plugins/load/load_multiverse_usd.py index 9e0d38df46..d08fcd904e 100644 --- a/openpype/hosts/maya/plugins/load/load_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/load/load_multiverse_usd.py @@ -36,6 +36,8 @@ class MultiverseUsdLoader(load.LoaderPlugin): suffix="_", ) + path = self.filepath_from_context(context) + # Make sure we can load the plugin cmds.loadPlugin("MultiverseForMaya", quiet=True) import multiverse @@ -46,7 +48,7 @@ class MultiverseUsdLoader(load.LoaderPlugin): with maintained_selection(): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): - shape = multiverse.CreateUsdCompound(self.fname) + shape = multiverse.CreateUsdCompound(path) transform = cmds.listRelatives( shape, parent=True, fullPath=True)[0] diff --git a/openpype/hosts/maya/plugins/load/load_multiverse_usd_over.py b/openpype/hosts/maya/plugins/load/load_multiverse_usd_over.py index 8a25508ac2..be8d78607b 100644 --- a/openpype/hosts/maya/plugins/load/load_multiverse_usd_over.py +++ b/openpype/hosts/maya/plugins/load/load_multiverse_usd_over.py @@ -50,9 +50,10 @@ class MultiverseUsdOverLoader(load.LoaderPlugin): cmds.loadPlugin("MultiverseForMaya", quiet=True) import multiverse + path = self.filepath_from_context(context) nodes = current_usd with maintained_selection(): - multiverse.AddUsdCompoundAssetPath(current_usd[0], self.fname) + multiverse.AddUsdCompoundAssetPath(current_usd[0], path) namespace = current_usd[0].split("|")[1].split(":")[0] diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index c288e23ded..a44482d21d 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -46,11 +46,11 @@ class RedshiftProxyLoader(load.LoaderPlugin): # Ensure Redshift for Maya is loaded. cmds.loadPlugin("redshift4maya", quiet=True) + path = self.filepath_from_context(context) with maintained_selection(): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): - nodes, group_node = self.create_rs_proxy( - name, self.fname) + nodes, group_node = self.create_rs_proxy(name, path) self[:] = nodes if not nodes: diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index deadd5b9d3..5c838fbc3c 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -122,9 +122,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): attach_to_root = options.get("attach_to_root", True) group_name = options["group_name"] + path = self.filepath_from_context(context) with maintained_selection(): cmds.loadPlugin("AbcImport.mll", quiet=True) - file_url = self.prepare_root_value(self.fname, + file_url = self.prepare_root_value(path, context["project"]["name"]) nodes = cmds.file(file_url, diff --git a/openpype/hosts/maya/plugins/load/load_rendersetup.py b/openpype/hosts/maya/plugins/load/load_rendersetup.py index 7a2d8b1002..8b85f11958 100644 --- a/openpype/hosts/maya/plugins/load/load_rendersetup.py +++ b/openpype/hosts/maya/plugins/load/load_rendersetup.py @@ -43,8 +43,9 @@ class RenderSetupLoader(load.LoaderPlugin): prefix="_" if asset[0].isdigit() else "", suffix="_", ) - self.log.info(">>> loading json [ {} ]".format(self.fname)) - with open(self.fname, "r") as file: + path = self.filepath_from_context(context) + self.log.info(">>> loading json [ {} ]".format(path)) + with open(path, "r") as file: renderSetup.instance().decode( json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py b/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py index 8a386cecfd..13fd156216 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py @@ -65,8 +65,9 @@ class LoadVDBtoArnold(load.LoaderPlugin): name="{}Shape".format(root), parent=root) + path = self.filepath_from_context(context) self._set_path(grid_node, - path=self.fname, + path=path, representation=context["representation"]) # Lock the shape node so the user can't delete the transform/shape diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py index 1f02321dc8..464d5b0ba8 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py @@ -85,7 +85,7 @@ class LoadVDBtoRedShift(load.LoaderPlugin): parent=root) self._set_path(volume_node, - path=self.fname, + path=self.filepath_from_context(context), representation=context["representation"]) nodes = [root, volume_node] diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py index 9267c59c02..69eb44a5e9 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -88,8 +88,9 @@ class LoadVDBtoVRay(load.LoaderPlugin): from openpype.hosts.maya.api.lib import unique_namespace from openpype.hosts.maya.api.pipeline import containerise - assert os.path.exists(self.fname), ( - "Path does not exist: %s" % self.fname + path = self.filepath_from_context(context) + assert os.path.exists(path), ( + "Path does not exist: %s" % path ) try: @@ -146,7 +147,7 @@ class LoadVDBtoVRay(load.LoaderPlugin): cmds.connectAttr("time1.outTime", grid_node + ".currentTime") # Set path - self._set_path(grid_node, self.fname, show_preset_popup=True) + self._set_path(grid_node, path, show_preset_popup=True) # Lock the shape node so the user can't delete the transform/shape # as if it was referenced diff --git a/openpype/hosts/maya/plugins/load/load_vrayproxy.py b/openpype/hosts/maya/plugins/load/load_vrayproxy.py index 64184f9e7b..77efdb2069 100644 --- a/openpype/hosts/maya/plugins/load/load_vrayproxy.py +++ b/openpype/hosts/maya/plugins/load/load_vrayproxy.py @@ -53,7 +53,9 @@ class VRayProxyLoader(load.LoaderPlugin): family = "vrayproxy" # get all representations for this version - self.fname = self._get_abc(context["version"]["_id"]) or self.fname + filename = self._get_abc(context["version"]["_id"]) + if not filename: + filename = self.filepath_from_context(context) asset_name = context['asset']["name"] namespace = namespace or unique_namespace( @@ -69,7 +71,7 @@ class VRayProxyLoader(load.LoaderPlugin): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): nodes, group_node = self.create_vray_proxy( - name, filename=self.fname) + name, filename=filename) self[:] = nodes if not nodes: @@ -190,7 +192,7 @@ class VRayProxyLoader(load.LoaderPlugin): if abc_rep: self.log.debug("Found, we'll link alembic to vray proxy.") file_name = get_representation_path(abc_rep) - self.log.debug("File: {}".format(self.fname)) + self.log.debug("File: {}".format(file_name)) return file_name return "" diff --git a/openpype/hosts/maya/plugins/load/load_vrayscene.py b/openpype/hosts/maya/plugins/load/load_vrayscene.py index d87992f9a7..f9169ff884 100644 --- a/openpype/hosts/maya/plugins/load/load_vrayscene.py +++ b/openpype/hosts/maya/plugins/load/load_vrayscene.py @@ -46,8 +46,10 @@ class VRaySceneLoader(load.LoaderPlugin): with maintained_selection(): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): - nodes, root_node = self.create_vray_scene(name, - filename=self.fname) + nodes, root_node = self.create_vray_scene( + name, + filename=self.filepath_from_context(context) + ) self[:] = nodes if not nodes: diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 16f2e8e842..323f8d7eda 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -48,7 +48,8 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): return maya_filepath = self.prepare_root_value( - self.fname, context["project"]["name"] + file_url=self.filepath_from_context(context), + project_name=context["project"]["name"] ) # Reference xgen. Xgen does not like being referenced in under a group. diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py index 5ba381050a..833be79ef2 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py @@ -60,7 +60,8 @@ class YetiCacheLoader(load.LoaderPlugin): cmds.loadPlugin("pgYetiMaya", quiet=True) # Create Yeti cache nodes according to settings - settings = self.read_settings(self.fname) + path = self.filepath_from_context(context) + settings = self.read_settings(path) nodes = [] for node in settings["nodes"]: nodes.extend(self.create_node(namespace, node)) diff --git a/openpype/hosts/maya/plugins/load/load_yeti_rig.py b/openpype/hosts/maya/plugins/load/load_yeti_rig.py index b8066871b0..c9dfe9478b 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_rig.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_rig.py @@ -20,9 +20,10 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self, context, name=None, namespace=None, options=None ): group_name = options['group_name'] + path = self.filepath_from_context(context) with lib.maintained_selection(): file_url = self.prepare_root_value( - self.fname, context["project"]["name"] + path, context["project"]["name"] ) nodes = cmds.file( file_url, diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index 67c7877e60..f4f581e6a4 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -72,7 +72,7 @@ class LoadBackdropNodes(load.LoaderPlugin): data_imprint.update({k: version_data[k]}) # getting file path - file = self.fname.replace("\\", "/") + file = self.filepath_from_context(context).replace("\\", "/") # adding nodes to node graph # just in case we are in group lets jump out of it diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index 40822c9eb7..951457475d 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -57,7 +57,7 @@ class AlembicCameraLoader(load.LoaderPlugin): data_imprint.update({k: version_data[k]}) # getting file path - file = self.fname.replace("\\", "/") + file = self.filepath_from_context(context).replace("\\", "/") with maintained_selection(): camera_node = nuke.createNode( diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 8f24fe6861..92f01e560a 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -99,7 +99,8 @@ class LoadClip(plugin.NukeLoader): representation = self._representation_with_hash_in_frame( representation ) - filepath = get_representation_path(representation).replace("\\", "/") + filepath = self.filepath_from_context(context) + filepath = filepath.replace("\\", "/") self.log.debug("_ filepath: {}".format(filepath)) start_at_workfile = options.get( @@ -154,7 +155,7 @@ class LoadClip(plugin.NukeLoader): read_node["file"].setValue(filepath) used_colorspace = self._set_colorspace( - read_node, version_data, representation["data"]) + read_node, version_data, representation["data"], filepath) self._set_range_to_node(read_node, first, last, start_at_workfile) @@ -306,8 +307,7 @@ class LoadClip(plugin.NukeLoader): # we will switch off undo-ing with viewer_update_and_undo_stop(): used_colorspace = self._set_colorspace( - read_node, version_data, representation["data"], - path=filepath) + read_node, version_data, representation["data"], filepath) self._set_range_to_node(read_node, first, last, start_at_workfile) @@ -454,9 +454,9 @@ class LoadClip(plugin.NukeLoader): return self.node_name_template.format(**name_data) - def _set_colorspace(self, node, version_data, repre_data, path=None): + def _set_colorspace(self, node, version_data, repre_data, path): output_color = None - path = path or self.fname.replace("\\", "/") + path = path.replace("\\", "/") # get colorspace colorspace = repre_data.get("colorspace") colorspace = colorspace or version_data.get("colorspace") diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index eb1c905c4d..6e56fe4a56 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -72,7 +72,7 @@ class LoadEffects(load.LoaderPlugin): data_imprint.update({k: version_data[k]}) # getting file path - file = self.fname.replace("\\", "/") + file = self.filepath_from_context(context).replace("\\", "/") # getting data from json file with unicode conversion with open(file, "r") as f: diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index 03be8654ed..95452919e2 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -73,7 +73,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): data_imprint.update({k: version_data[k]}) # getting file path - file = self.fname.replace("\\", "/") + file = self.filepath_from_context(context).replace("\\", "/") # getting data from json file with unicode conversion with open(file, "r") as f: diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo.py b/openpype/hosts/nuke/plugins/load/load_gizmo.py index 2aa7c49723..c7a40e0d00 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo.py @@ -73,7 +73,7 @@ class LoadGizmo(load.LoaderPlugin): data_imprint.update({k: version_data[k]}) # getting file path - file = self.fname.replace("\\", "/") + file = self.filepath_from_context(context).replace("\\", "/") # adding nodes to node graph # just in case we are in group lets jump out of it diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index 2514a28299..ceb8ee9609 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -75,7 +75,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): data_imprint.update({k: version_data[k]}) # getting file path - file = self.fname.replace("\\", "/") + file = self.filepath_from_context(context).replace("\\", "/") # adding nodes to node graph # just in case we are in group lets jump out of it diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 0a79ddada7..0ff39482d9 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -86,7 +86,7 @@ class LoadImage(load.LoaderPlugin): if namespace is None: namespace = context['asset']['name'] - file = self.fname + file = self.filepath_from_context(context) if not file: repr_id = context["representation"]["_id"] diff --git a/openpype/hosts/nuke/plugins/load/load_matchmove.py b/openpype/hosts/nuke/plugins/load/load_matchmove.py index a7d124d472..f942422c00 100644 --- a/openpype/hosts/nuke/plugins/load/load_matchmove.py +++ b/openpype/hosts/nuke/plugins/load/load_matchmove.py @@ -18,8 +18,9 @@ class MatchmoveLoader(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, data): - if self.fname.lower().endswith(".py"): - exec(open(self.fname).read()) + path = self.filepath_from_context(context) + if path.lower().endswith(".py"): + exec(open(path).read()) else: msg = "Unsupported script type" diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index 36781993ea..fad29d0d0b 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -55,7 +55,7 @@ class AlembicModelLoader(load.LoaderPlugin): data_imprint.update({k: version_data[k]}) # getting file path - file = self.fname.replace("\\", "/") + file = self.filepath_from_context(context).replace("\\", "/") with maintained_selection(): model_node = nuke.createNode( diff --git a/openpype/hosts/nuke/plugins/load/load_script_precomp.py b/openpype/hosts/nuke/plugins/load/load_script_precomp.py index b74fdf481a..7fd7c13c48 100644 --- a/openpype/hosts/nuke/plugins/load/load_script_precomp.py +++ b/openpype/hosts/nuke/plugins/load/load_script_precomp.py @@ -43,8 +43,8 @@ class LinkAsGroup(load.LoaderPlugin): if namespace is None: namespace = context['asset']['name'] - file = self.fname.replace("\\", "/") - self.log.info("file: {}\n".format(self.fname)) + file = self.filepath_from_context(context).replace("\\", "/") + self.log.info("file: {}\n".format(file)) precomp_name = context["representation"]["context"]["subset"] diff --git a/openpype/hosts/photoshop/api/README.md b/openpype/hosts/photoshop/api/README.md index 4a36746cb2..7bd2bcb1bf 100644 --- a/openpype/hosts/photoshop/api/README.md +++ b/openpype/hosts/photoshop/api/README.md @@ -210,8 +210,9 @@ class ImageLoader(load.LoaderPlugin): representations = ["*"] def load(self, context, name=None, namespace=None, data=None): + path = self.filepath_from_context(context) with photoshop.maintained_selection(): - layer = stub.import_smart_object(self.fname) + layer = stub.import_smart_object(path) self[:] = [layer] diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index 91a9787781..eb770bbd20 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -22,7 +22,8 @@ class ImageLoader(photoshop.PhotoshopLoader): name ) with photoshop.maintained_selection(): - layer = self.import_layer(self.fname, layer_name, stub) + path = self.filepath_from_context(context) + layer = self.import_layer(path, layer_name, stub) self[:] = [layer] namespace = namespace or layer_name diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index c25c5a8f2c..f9fceb80bb 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -29,11 +29,13 @@ class ImageFromSequenceLoader(photoshop.PhotoshopLoader): options = [] def load(self, context, name=None, namespace=None, data=None): + + path = self.filepath_from_context(context) if data.get("frame"): - self.fname = os.path.join( - os.path.dirname(self.fname), data["frame"] + path = os.path.join( + os.path.dirname(path), data["frame"] ) - if not os.path.exists(self.fname): + if not os.path.exists(path): return stub = self.get_stub() @@ -42,7 +44,7 @@ class ImageFromSequenceLoader(photoshop.PhotoshopLoader): ) with photoshop.maintained_selection(): - layer = stub.import_smart_object(self.fname, layer_name) + layer = stub.import_smart_object(path, layer_name) self[:] = [layer] namespace = namespace or layer_name diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py index 1f32a5d23c..5772e243d5 100644 --- a/openpype/hosts/photoshop/plugins/load/load_reference.py +++ b/openpype/hosts/photoshop/plugins/load/load_reference.py @@ -23,7 +23,8 @@ class ReferenceLoader(photoshop.PhotoshopLoader): stub.get_layers(), context["asset"]["name"], name ) with photoshop.maintained_selection(): - layer = self.import_layer(self.fname, layer_name, stub) + path = self.filepath_from_context(context) + layer = self.import_layer(path, layer_name, stub) self[:] = [layer] namespace = namespace or layer_name diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index e5846c2fc2..59c27f29da 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -291,7 +291,7 @@ class ClipLoader: active_bin = None data = dict() - def __init__(self, cls, context, **options): + def __init__(self, cls, context, path, **options): """ Initialize object Arguments: @@ -304,6 +304,7 @@ class ClipLoader: self.__dict__.update(cls.__dict__) self.context = context self.active_project = lib.get_current_project() + self.fname = path # try to get value from options or evaluate key value for `handles` self.with_handles = options.get("handles") or bool( diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 05bfb003d6..3518597e8c 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -55,8 +55,9 @@ class LoadClip(plugin.TimelineItemLoader): }) # load clip to timeline and get main variables + path = self.filepath_from_context(context) timeline_item = plugin.ClipLoader( - self, context, **options).load() + self, context, path, **options).load() namespace = namespace or timeline_item.GetName() version = context['version'] version_data = version.get("data", {}) @@ -115,10 +116,10 @@ class LoadClip(plugin.TimelineItemLoader): version_name = version.get("name", None) colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) - self.fname = get_representation_path(representation) + path = get_representation_path(representation) context["version"] = {"data": version_data} - loader = plugin.ClipLoader(self, context) + loader = plugin.ClipLoader(self, context, path) timeline_item = loader.update(timeline_item) # add additional metadata from the version to imprint Avalon knob diff --git a/openpype/hosts/tvpaint/plugins/load/load_image.py b/openpype/hosts/tvpaint/plugins/load/load_image.py index 5283d04355..a400738019 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_image.py @@ -77,8 +77,9 @@ class ImportImage(plugin.Loader): ) # Fill import script with filename and layer name # - filename mus not contain backwards slashes + path = self.filepath_from_context(context).replace("\\", "/") george_script = self.import_script.format( - self.fname.replace("\\", "/"), + path, layer_name, load_options_str ) diff --git a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py index 7f7a68cc41..edc116a8e4 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py @@ -86,10 +86,12 @@ class LoadImage(plugin.Loader): subset_name = context["subset"]["name"] layer_name = self.get_unique_layer_name(asset_name, subset_name) + path = self.filepath_from_context(context) + # Fill import script with filename and layer name # - filename mus not contain backwards slashes george_script = self.import_script.format( - self.fname.replace("\\", "/"), + path.replace("\\", "/"), layer_name, load_options_str ) @@ -271,9 +273,6 @@ class LoadImage(plugin.Loader): # Remove old layers self._remove_layers(layer_ids=layer_ids_to_remove) - # Change `fname` to new representation - self.fname = self.filepath_from_context(context) - name = container["name"] namespace = container["namespace"] new_container = self.load(context, name, namespace, {}) diff --git a/openpype/hosts/tvpaint/plugins/load/load_sound.py b/openpype/hosts/tvpaint/plugins/load/load_sound.py index f312db262a..3003280eef 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_sound.py +++ b/openpype/hosts/tvpaint/plugins/load/load_sound.py @@ -60,9 +60,10 @@ class ImportSound(plugin.Loader): output_filepath = output_file.name.replace("\\", "/") # Prepare george script + path = self.filepath_from_context(context).replace("\\", "/") import_script = "\n".join(self.import_script_lines) george_script = import_script.format( - self.fname.replace("\\", "/"), + path, output_filepath ) self.log.info("*** George script:\n{}\n***".format(george_script)) diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py index fc7588f56e..9492d3d5eb 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_workfile.py +++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py @@ -31,18 +31,18 @@ class LoadWorkfile(plugin.Loader): def load(self, context, name, namespace, options): # Load context of current workfile as first thing # - which context and extension has - host = registered_host() - current_file = host.get_current_workfile() - - context = get_current_workfile_context() - - filepath = self.fname.replace("\\", "/") + filepath = self.filepath_from_context(context) + filepath = filepath.replace("\\", "/") if not os.path.exists(filepath): raise FileExistsError( "The loaded file does not exist. Try downloading it first." ) + host = registered_host() + current_file = host.get_current_workfile() + work_context = get_current_workfile_context() + george_script = "tv_LoadProject '\"'\"{}\"'\"'".format( filepath ) @@ -50,10 +50,10 @@ class LoadWorkfile(plugin.Loader): # Save workfile. host_name = "tvpaint" - project_name = context.get("project") - asset_name = context.get("asset") - task_name = context.get("task") - # Far cases when there is workfile without context + project_name = work_context.get("project") + asset_name = work_context.get("asset") + task_name = work_context.get("task") + # Far cases when there is workfile without work_context if not asset_name: project_name = legacy_io.active_project() asset_name = legacy_io.Session["AVALON_ASSET"] diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index 52eea4122a..cb60197a4c 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -87,7 +87,8 @@ class AnimationAlembicLoader(plugin.Loader): if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): unreal.EditorAssetLibrary.make_directory(asset_dir) - task = self.get_task(self.fname, asset_dir, asset_name, False) + path = self.filepath_from_context(context) + task = self.get_task(path, asset_dir, asset_name, False) asset_tools = unreal.AssetToolsHelpers.get_asset_tools() asset_tools.import_asset_tasks([task]) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index a5ecb677e8..7ed85ee411 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -26,7 +26,7 @@ class AnimationFBXLoader(plugin.Loader): icon = "cube" color = "orange" - def _process(self, asset_dir, asset_name, instance_name): + def _process(self, path, asset_dir, asset_name, instance_name): automated = False actor = None @@ -55,7 +55,7 @@ class AnimationFBXLoader(plugin.Loader): asset_doc = get_current_project_asset(fields=["data.fps"]) - task.set_editor_property('filename', self.fname) + task.set_editor_property('filename', path) task.set_editor_property('destination_path', asset_dir) task.set_editor_property('destination_name', asset_name) task.set_editor_property('replace_existing', False) @@ -177,14 +177,15 @@ class AnimationFBXLoader(plugin.Loader): EditorAssetLibrary.make_directory(asset_dir) - libpath = self.fname.replace("fbx", "json") + path = self.filepath_from_context(context) + libpath = path.replace(".fbx", ".json") with open(libpath, "r") as fp: data = json.load(fp) instance_name = data.get("instance_name") - animation = self._process(asset_dir, asset_name, instance_name) + animation = self._process(path, asset_dir, asset_name, instance_name) asset_content = EditorAssetLibrary.list_assets( hierarchy_dir, recursive=True, include_folder=False) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 59ea14697d..3d98b3d8e1 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -200,12 +200,13 @@ class CameraLoader(plugin.Loader): settings.set_editor_property('reduce_keys', False) if cam_seq: + path = self.filepath_from_context(context) self._import_camera( EditorLevelLibrary.get_editor_world(), cam_seq, cam_seq.get_bindings(), settings, - self.fname + path ) # Set range of all sections diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 3a292fdbd1..13ba236a7d 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -111,8 +111,9 @@ class PointCacheAlembicLoader(plugin.Loader): if frame_start == frame_end: frame_end += 1 + path = self.filepath_from_context(context) task = self.get_task( - self.fname, asset_dir, asset_name, False, frame_start, frame_end) + path, asset_dir, asset_name, False, frame_start, frame_end) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 86b2e1456c..e9f3c79960 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -618,7 +618,8 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.load_level(level) - loaded_assets = self._process(self.fname, asset_dir, shot) + path = self.filepath_from_context(context) + loaded_assets = self._process(path, asset_dir, shot) for s in sequences: EditorAssetLibrary.save_asset(s.get_path_name()) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 929a9a1399..32fff84152 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -380,7 +380,8 @@ class ExistingLayoutLoader(plugin.Loader): raise AssertionError("Current level not saved") project_name = context["project"]["name"] - containers = self._process(self.fname, project_name) + path = self.filepath_from_context(context) + containers = self._process(path, project_name) curr_level_path = Path( curr_level.get_outer().get_path_name()).parent.as_posix() diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 7591d5582f..0b0030ff77 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -89,7 +89,8 @@ class SkeletalMeshAlembicLoader(plugin.Loader): if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): unreal.EditorAssetLibrary.make_directory(asset_dir) - task = self.get_task(self.fname, asset_dir, asset_name, False) + path = self.filepath_from_context(context) + task = self.get_task(path, asset_dir, asset_name, False) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index e9676cde3a..09cd37b9db 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -23,7 +23,7 @@ class SkeletalMeshFBXLoader(plugin.Loader): def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. - This is two step process. First, import FBX to temporary path and + This is a two step process. First, import FBX to temporary path and then call `containerise()` on it - this moves all content to new directory and then it will create AssetContainer there and imprint it with metadata. This will mark this path as container. @@ -65,7 +65,8 @@ class SkeletalMeshFBXLoader(plugin.Loader): task = unreal.AssetImportTask() - task.set_editor_property('filename', self.fname) + path = self.filepath_from_context(context) + task.set_editor_property('filename', path) task.set_editor_property('destination_path', asset_dir) task.set_editor_property('destination_name', asset_name) task.set_editor_property('replace_existing', False) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index befc7b0ac9..98e6d962b1 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -98,8 +98,9 @@ class StaticMeshAlembicLoader(plugin.Loader): if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): unreal.EditorAssetLibrary.make_directory(asset_dir) + path = self.filepath_from_context(context) task = self.get_task( - self.fname, asset_dir, asset_name, False, default_conversion) + path, asset_dir, asset_name, False, default_conversion) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index e416256486..fa26e252f5 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -88,7 +88,8 @@ class StaticMeshFBXLoader(plugin.Loader): unreal.EditorAssetLibrary.make_directory(asset_dir) - task = self.get_task(self.fname, asset_dir, asset_name, False) + path = self.filepath_from_context(context) + task = self.get_task(path, asset_dir, asset_name, False) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 30f63abe39..88aaac41e8 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -64,8 +64,9 @@ class UAssetLoader(plugin.Loader): destination_path = asset_dir.replace( "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) + path = self.filepath_from_context(context) shutil.copy( - self.fname, + path, f"{destination_path}/{name}_{unique_number:02}.{self.extension}") # Create Asset Container diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index e380d65bbe..f87fb3312d 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -39,9 +39,6 @@ class LoaderPlugin(list): log = logging.getLogger("SubsetLoader") log.propagate = True - def __init__(self, context): - self.fname = self.filepath_from_context(context) - @classmethod def apply_settings(cls, project_settings, system_settings): host_name = os.environ.get("AVALON_APP") @@ -246,9 +243,6 @@ class SubsetLoaderPlugin(LoaderPlugin): namespace (str, optional): Use pre-defined namespace """ - def __init__(self, context): - pass - def discover_loader_plugins(project_name=None): from openpype.lib import Logger diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 2c40280ccd..42418be40e 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -314,7 +314,12 @@ def load_with_repre_context( ) ) - loader = Loader(repre_context) + loader = Loader() + + # Backwards compatibility: Originally the loader's __init__ required the + # representation context to set `fname` attribute to the filename to load + loader.fname = get_representation_path_from_context(repre_context) + return loader.load(repre_context, name, namespace, options) @@ -338,8 +343,7 @@ def load_with_subset_context( ) ) - loader = Loader(subset_context) - return loader.load(subset_context, name, namespace, options) + return Loader().load(subset_context, name, namespace, options) def load_with_subset_contexts( @@ -364,8 +368,7 @@ def load_with_subset_contexts( "Running '{}' on '{}'".format(Loader.__name__, joined_subset_names) ) - loader = Loader(subset_contexts) - return loader.load(subset_contexts, name, namespace, options) + return Loader().load(subset_contexts, name, namespace, options) def load_container( @@ -447,8 +450,7 @@ def remove_container(container): .format(container.get("loader")) ) - loader = Loader(get_representation_context(container["representation"])) - return loader.remove(container) + return Loader().remove(container) def update_container(container, version=-1): @@ -498,8 +500,7 @@ def update_container(container, version=-1): .format(container.get("loader")) ) - loader = Loader(get_representation_context(container["representation"])) - return loader.update(container, new_representation) + return Loader().update(container, new_representation) def switch_container(container, representation, loader_plugin=None): @@ -635,7 +636,7 @@ def get_representation_path(representation, root=None, dbcon=None): root = registered_root() - def path_from_represenation(): + def path_from_representation(): try: template = representation["data"]["template"] except KeyError: @@ -759,7 +760,7 @@ def get_representation_path(representation, root=None, dbcon=None): return os.path.normpath(path) return ( - path_from_represenation() or + path_from_representation() or path_from_config() or path_from_data() ) diff --git a/openpype/plugins/load/copy_file.py b/openpype/plugins/load/copy_file.py index 163f56a83a..7fd56c8a6a 100644 --- a/openpype/plugins/load/copy_file.py +++ b/openpype/plugins/load/copy_file.py @@ -14,8 +14,9 @@ class CopyFile(load.LoaderPlugin): color = get_default_entity_icon_color() def load(self, context, name=None, namespace=None, data=None): - self.log.info("Added copy to clipboard: {0}".format(self.fname)) - self.copy_file_to_clipboard(self.fname) + path = self.filepath_from_context(context) + self.log.info("Added copy to clipboard: {0}".format(path)) + self.copy_file_to_clipboard(path) @staticmethod def copy_file_to_clipboard(path): diff --git a/openpype/plugins/load/copy_file_path.py b/openpype/plugins/load/copy_file_path.py index 569e5c8780..b055494e85 100644 --- a/openpype/plugins/load/copy_file_path.py +++ b/openpype/plugins/load/copy_file_path.py @@ -14,8 +14,9 @@ class CopyFilePath(load.LoaderPlugin): color = "#999999" def load(self, context, name=None, namespace=None, data=None): - self.log.info("Added file path to clipboard: {0}".format(self.fname)) - self.copy_path_to_clipboard(self.fname) + path = self.filepath_from_context(context) + self.log.info("Added file path to clipboard: {0}".format(path)) + self.copy_path_to_clipboard(path) @staticmethod def copy_path_to_clipboard(path): diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index 9c36e7f405..5c679f6a51 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -33,9 +33,11 @@ class OpenInDJV(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, data): - directory = os.path.dirname(self.fname) import clique + path = self.filepath_from_context(context) + directory = os.path.dirname(path) + pattern = clique.PATTERNS["frames"] files = os.listdir(directory) collections, remainder = clique.assemble( @@ -48,7 +50,7 @@ class OpenInDJV(load.LoaderPlugin): sequence = collections[0] first_image = list(sequence)[0] else: - first_image = self.fname + first_image = path filepath = os.path.normpath(os.path.join(directory, first_image)) self.log.info("Opening : {}".format(filepath)) diff --git a/openpype/plugins/load/open_file.py b/openpype/plugins/load/open_file.py index 00b2ecd7c5..5c4f4901d1 100644 --- a/openpype/plugins/load/open_file.py +++ b/openpype/plugins/load/open_file.py @@ -28,7 +28,7 @@ class OpenFile(load.LoaderPlugin): def load(self, context, name, namespace, data): - path = self.fname + path = self.filepath_from_context(context) if not os.path.exists(path): raise RuntimeError("File not found: {}".format(path)) diff --git a/website/docs/dev_colorspace.md b/website/docs/dev_colorspace.md index c4b8e74d73..cb07cb18a0 100644 --- a/website/docs/dev_colorspace.md +++ b/website/docs/dev_colorspace.md @@ -80,7 +80,7 @@ from openpype.pipeline.colorspace import ( class YourLoader(api.Loader): def load(self, context, name=None, namespace=None, options=None): - path = self.fname + path = self.filepath_from_context(context) colorspace_data = context["representation"]["data"].get("colorspaceData", {}) colorspace = ( colorspace_data.get("colorspace") From 33148f45e7af23f1e0607c656154a67a907d3934 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 21 Mar 2023 16:49:45 +0100 Subject: [PATCH 203/446] General: Environment variable for default OCIO configs (#4670) * set 'BUILTIN_OCIO_ROOT' environment variable in start.py We need to use simplified variant to fill default ocio root for OCIO configs to replace '{OPENPYPE_ROOT}/vendor/bin/ocioconfig/OpenColorIOConfigs' in settings. The explicit path from OPENPYPE_ROOT disable option to change the path elsewhere without harm of settings change. Using 'BUILTIN_OCIO_ROOT' can be usd universally. * use new env variable in OCIO settings --- openpype/settings/defaults/project_settings/global.json | 4 ++-- start.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 802b964375..0da1e0ea74 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -3,8 +3,8 @@ "activate_global_color_management": false, "ocio_config": { "filepath": [ - "{OPENPYPE_ROOT}/vendor/bin/ocioconfig/OpenColorIOConfigs/aces_1.2/config.ocio", - "{OPENPYPE_ROOT}/vendor/bin/ocioconfig/OpenColorIOConfigs/nuke-default/config.ocio" + "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", + "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio" ] }, "file_rules": { diff --git a/start.py b/start.py index 4849a241d2..36e2540200 100644 --- a/start.py +++ b/start.py @@ -197,6 +197,15 @@ if "--headless" in sys.argv: elif os.getenv("OPENPYPE_HEADLESS_MODE") != "1": os.environ.pop("OPENPYPE_HEADLESS_MODE", None) +# Set builtin ocio root +os.environ["BUILTIN_OCIO_ROOT"] = os.path.join( + OPENPYPE_ROOT, + "vendor", + "bin", + "ocioconfig", + "OpenColorIOConfigs" +) + # Enabled logging debug mode when "--debug" is passed if "--verbose" in sys.argv: expected_values = ( From 47473a8a23deb3241ebbc5f53ab16b457c482ff9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 Mar 2023 14:21:05 +0100 Subject: [PATCH 204/446] General: Connect to AYON server (base) (#3924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implemented 'get_workfile_info' in entities * removed 'prepare_asset_update_data' which is not used * disable settings and project manager if in v4 mode * prepared conversion helper functions for v4 entities * prepared conversion functions for hero versions * fix hero versions * implemented get_archived_representations * fix get latest versions * return prepared changes * handle archived representation * raise exception on failed json conversion * map archived to active properly * make sure default fields are added * fix conversion of hero version entity * fix conversion of archived representations * fix some conversions of representations and versions * changed active behavior in queries * fixed hero versions * implemented basic thumbnail caching * added raw variants of crud methods * implemented methods to get and create thumbnail * fix from flat dict * implemented some basic folder conversion for updates * fix thumbnail updates for version * implemented v4 thumbnail integrator * simplified data mapping * 'get_thumbnail' function also expect entity type and entity id for which is the thumbnail received * implemented 'get_thumbnail' for server * fix how thumbnail id is received from entity * removed unnecessary method 'get_thumbnail_id_from_source' * implemented thumbnail resolver for v4 * removed unnecessary print * move create and delete project directly to server api * disable local settings action too on v4 * OP-3521 - added method to check and download updated addons from v4 server * OP-3521 - added more descriptive error message for missing source * OP-3521 - added default implementation of addon downloader to import * OP-3521 - added check for dependency package zips WIP - server doesn't contain required endpoint. Testing only with mockup data for now. * OP-3521 - fixed parsing of DependencyItem Added Server Url type and ServerAddonDownloader - v4 server doesn't know its own DNS for static files so it is sending unique name and url must be created during runtime. * OP-3521 - fixed creation of targed directories * change nev keys to look for and don't set them automatically * fix task type conversion * implemented base of loading v4 addons in v3 * Refactored argument name in Downloaders * Updated parsing to DependencyItem according to current schema * Implemented downloading of package from server * Updated resolving of failures Uses Enum items. * Introduced passing of authorization token Better to inject it than to have it from env var. * Remove weird parsing of server_url Not necessary, endpoints have same prefix. * Fix doubling asset version name in addons folder Zip file should already contain `addonName_addonVersion` as first subfolder * Fix doubling asset version name in addons folder Zip file should already contain `addonName_addonVersion` as first subfolder * Made server_endpoint optional Argument should be better for testing, but for calling from separate methods it would be better to encapsulate it. Removed unwanted temporary productionPackage value * Use existing method to pull addon info from Server to load v4 version of addon * Raise exception when server doesn't have any production dependency package * added ability to specify v3 alias of addon name * expect v3_alias as uppered constant * Re-implemented method to get addon info Previous implementation wouldn't work in Python2 hosts. Will be refactored in the future. * fix '__getattr__' * added ayon api to pyproject.toml and lock file * use ayon api in common connection * added mapping for label * use ayon_api in client codebase * separated clearing cache of url and username * bump ayon api version * rename env 'OP4_TEST' to 'USE_AYON_SERVER' * Move and renamend get_addons_info to get_addons_info_as_dict in addon_distribution Should be moved to ayon_api later * Replaced requests calls with ayon_api * Replaced OP4_TEST_ENABLED with AYON_SERVER_ENABLED fixed endpoints * Hound * Hound * OP-3521 - fix wrong key in get_representation_parents parents overloads parents * OP-3521 - changes for v4 of SiteSync addon * OP-3521 - fix names * OP-3521 - remove storing project_name It should be safer to go thorug self.dbcon apparently * OP-3521 - remove unwanted "context["folder"]" can be only in dummy test data * OP-3521 - move site sync loaders to addon * Use only project instead of self.project * OP-3521 - added missed get_progress_for_repre * base of settings conversion script * simplified ayon functions in start.py * added loading of settings from ayon server * added a note about colors * fix global and local settings functions * AvalonMongoDB is not using mongo connection on ayon server enabled * 'get_dynamic_modules_dirs' is not checking system settings for paths in setting * log viewer is disabled when ayon server is enabled * basic logic of enabling/disabled addons * don't use mongo logging if ayon server is enabled * update ayon api * bump ayon api again * use ayon_api to get addons info in modules/base * update ayon api * moved helper functions to get addons and dependencies dir to common functions * Initialization of AddonInfo is not crashing on unkonwn sources * renamed 'DependencyDownloader' to 'AyonServerDownloader' * renamed function 'default_addon_downloader' to 'get_default_addon_downloader' * Added ability to convert 'WebAddonSource' to 'ServerResourceSorce' * missing dependency package on server won't cause crash * data sent to downloaders don't contain ayon specific headers * modified addon distribution to not duplicate 'ayon_api' functionality * fix doubled function defintioin * unzip client file to addon destination * formatting - unify quotes * disable usage of mongo connection if in ayon mode * renamed window.py to login_window.py * added webpublisher settings conversion * added maya conversion function * reuse variable * reuse variable (similar to previous commit) * fix ayon addons loading * fix typo 'AyonSettingsCahe' -> 'AyonSettingsCache' * fix enabled state changes * fix rr_path in royal render conversion * avoid mongo calls in AYON state * implemented custom AYON start script * fix formatting (after black) * ayon_start cleanup * 'get_addons_dir' and 'get_dependencies_dir' store value to environment variable * add docstrings to local dir functions * addon info has full name * fix modules enabled states * removed unused 'run_disk_mapping_commands' * removed ayon logic from 'start.py' * fix warning message * renamed 'openpype_common' to 'ayon_common' * removed unused import * don't import igniter * removed startup validations of third parties * change what's shown in version info * fix which keys are applied from ayon values * fix method name * get applications from attribs * Implemented UI basics to be able change user or logout * merged server.py and credentials.py * add more metadata to urls * implemented change token * implemented change user ui functionality * implemented change user ui * modify window to handle username and token value * pass username to add server * fix show UI cases * added loggin action to tray * update ayon api * added missing dependency * convert applications to config in a right way * initial implementation of 'nuke' settings conversion * removed few nuke comments * implemented hiero conversion * added imageio conversion * added run ayon tray script * fix few settings conversions * Renamed class of source classes as they are not just for addons * implemented objec to track source transfer progress * Implemented distribution item with multiple sources * Implemented ayon distribution wrapper to care about multiple things during distribution * added 'cleanup' method for downlaoders * download gets tranfer progress object * Change UploadState enum * added missing imports * use AyonDistribution in ayon_start.py * removed unused functions * removed implemented TODOs * fix import * fix key used for Web source * removed temp development fix * formatting fix * keep information if source require distribution * handle 'require_distribution' attribute in distribution process * added path attribute to server source * added option to pass addons infor to ayon distribution * fix tests * fix formatting * Fix typo * Fix typo * remove '_try_convert_to_server_source' * renamed attributes and methods to match their content * it is possible to pass dependency package info to AyonDistribution * fix called methods in tests * added public properties for error message and error detail * Added filename to WebSourceInfo Useful for GDrive sharable links where target file name is unknown/unparsable, it should be provided explicitly. * unify source conversion by adding 'convert_source' function * Fix error message Co-authored-by: Roy Nieterau * added docstring for 'transfer_progress' * don't create metadata file on read * added few docstrings * add default folder fields to folder/task queries * fix generators * add dependencies when runnign from code * add sys paths from distribution to pythonpath env * fix missing applications * added missing conversions for maya renderers * fix formatting * update ayon api * fix hashes in lock file * Use better exception Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> * Use Python 3 syntax Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> * apply some of sugested changes in ayon_start * added some docstrings and suggested modifications * copy create env from develop * fix rendersettings conversion * change code by suggestions * added missing args to docstring * added missing docstrings * separated downloader and download factory * fix ayon settings * added some basic file docstring to ayon_settings * join else conditions * fix project settings conversion * fix created at conversion * fix workfile info query * fix publisher UI * added utils function 'get_ayon_appdirs' * fix 'get_all_current_info' * fix server url assignment when url is set * updated ayon api * added utils functions to create local site id for ayon * added helper functions to create global connection * create global connection in ayon start to start use site id * use ayon site id in ayon mode * formatting cleanup * added header docstring * fixes after ayon_api update * load addons from ynput appdirs * fix function call * added docstring * update ayon pyton api * fix settings access * use ayon_api to get root overrides in Anatomy * bumbayon version to 0.1.13 * nuke: fixing settings keys from settings * fix burnins definitions * change v4 to AYON in thumbnail integrate * fix one more v4 information * Fixes after rebase * fix extract burnin conversion * additional fix of extract burnin * SiteSync:added missed loaders or v3 compatibility (#4587) * Added site sync loaders for v3 compatibility * Fix get_progress_for_repre * use 'files.name' instead of 'files.baseName' * update ayon api to 0.1.14 * add common to include files * change arguments for hero version creation * skip shotgrid settings conversion if different ayon addon is used * added ayon icons * fix labels of application variants * added option to show login window always on top * login window on invalid credentials is always on top * update ayon api * update ayon api * add entityType to project and folders * AYON: Editorial hierarchy creation (#4699) * disable extract hierarchy avalon when ayon mode is enabled * implemented extract hierarchy to AYON --------- Co-authored-by: Petr Kalis Co-authored-by: Roy Nieterau Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> Co-authored-by: Jakub Jezek --- ayon_start.py | 376 ++++ .../connection}/__init__.py | 0 common/ayon_common/connection/credentials.py | 475 +++++ common/ayon_common/connection/ui/__init__.py | 12 + common/ayon_common/connection/ui/lib.py | 11 + .../ayon_common/connection/ui/login_window.py | 753 ++++++++ common/ayon_common/connection/ui/widgets.py | 47 + .../distribution/README.md | 0 common/ayon_common/distribution/__init__.py | 0 .../distribution/addon_distribution.py | 1189 +++++++++++++ common/ayon_common/distribution/addon_info.py | 177 ++ .../distribution/file_handler.py | 46 +- .../tests/test_addon_distributtion.py | 76 +- common/ayon_common/resources/AYON.icns | Bin 0 -> 40634 bytes common/ayon_common/resources/AYON.ico | Bin 0 -> 4286 bytes common/ayon_common/resources/AYON.png | Bin 0 -> 16907 bytes common/ayon_common/resources/__init__.py | 21 + common/ayon_common/resources/edit.png | Bin 0 -> 9138 bytes common/ayon_common/resources/eye.png | Bin 0 -> 2152 bytes common/ayon_common/resources/stylesheet.css | 84 + common/ayon_common/utils.py | 52 + .../distribution/addon_distribution.py | 208 --- .../distribution/addon_info.py | 80 - openpype/__init__.py | 2 + openpype/client/entities.py | 1557 +---------------- openpype/client/entity_links.py | 247 +-- openpype/client/mongo/__init__.py | 20 + openpype/client/mongo/entities.py | 1553 ++++++++++++++++ openpype/client/mongo/entity_links.py | 244 +++ openpype/client/{ => mongo}/mongo.py | 3 + openpype/client/mongo/operations.py | 632 +++++++ openpype/client/operations.py | 815 +-------- openpype/client/operations_base.py | 289 +++ openpype/client/server/__init__.py | 0 openpype/client/server/constants.py | 83 + openpype/client/server/conversion_utils.py | 1244 +++++++++++++ openpype/client/server/entities.py | 655 +++++++ openpype/client/server/entity_links.py | 65 + openpype/client/server/openpype_comp.py | 156 ++ openpype/client/server/operations.py | 863 +++++++++ openpype/client/server/utils.py | 109 ++ openpype/hosts/nuke/api/lib.py | 12 +- openpype/lib/execute.py | 19 +- openpype/lib/local_settings.py | 44 + openpype/lib/log.py | 19 +- openpype/lib/pype_info.py | 25 +- openpype/modules/base.py | 121 +- .../modules/log_viewer/log_view_module.py | 3 + openpype/modules/project_manager_action.py | 4 + openpype/modules/settings_action.py | 5 + .../sync_server}/plugins/load/add_site.py | 0 .../sync_server}/plugins/load/remove_site.py | 0 openpype/modules/sync_server/sync_server.py | 4 +- .../modules/sync_server/sync_server_module.py | 63 +- openpype/pipeline/anatomy.py | 12 +- openpype/pipeline/legacy_io.py | 6 +- openpype/pipeline/mongodb.py | 4 +- openpype/pipeline/thumbnail.py | 28 + .../publish/extract_hierarchy_avalon.py | 4 + .../publish/extract_hierarchy_to_ayon.py | 234 +++ .../plugins/publish/integrate_hero_version.py | 20 +- .../plugins/publish/integrate_thumbnail.py | 5 + .../publish/integrate_thumbnail_ayon.py | 207 +++ openpype/resources/__init__.py | 15 +- openpype/resources/icons/AYON_icon.png | Bin 0 -> 16907 bytes openpype/resources/icons/AYON_splash.png | Bin 0 -> 20380 bytes openpype/settings/ayon_settings.py | 1141 ++++++++++++ openpype/settings/handlers.py | 8 +- openpype/settings/lib.py | 141 +- openpype/tools/loader/model.py | 6 +- openpype/tools/loader/widgets.py | 4 +- openpype/tools/sceneinventory/lib.py | 52 - openpype/tools/sceneinventory/model.py | 5 +- openpype/tools/sceneinventory/view.py | 3 +- openpype/tools/tray/pype_info_widget.py | 90 +- openpype/tools/tray/pype_tray.py | 19 + openpype/tools/utils/lib.py | 55 - poetry.lock | 35 +- pyproject.toml | 2 + setup.py | 33 +- start.py | 4 + tests/lib/testing_classes.py | 2 +- tools/run_tray_ayon.ps1 | 41 + 83 files changed, 11472 insertions(+), 3167 deletions(-) create mode 100644 ayon_start.py rename common/{openpype_common/distribution => ayon_common/connection}/__init__.py (100%) create mode 100644 common/ayon_common/connection/credentials.py create mode 100644 common/ayon_common/connection/ui/__init__.py create mode 100644 common/ayon_common/connection/ui/lib.py create mode 100644 common/ayon_common/connection/ui/login_window.py create mode 100644 common/ayon_common/connection/ui/widgets.py rename common/{openpype_common => ayon_common}/distribution/README.md (100%) create mode 100644 common/ayon_common/distribution/__init__.py create mode 100644 common/ayon_common/distribution/addon_distribution.py create mode 100644 common/ayon_common/distribution/addon_info.py rename common/{openpype_common => ayon_common}/distribution/file_handler.py (86%) rename common/{openpype_common => ayon_common}/distribution/tests/test_addon_distributtion.py (63%) create mode 100644 common/ayon_common/resources/AYON.icns create mode 100644 common/ayon_common/resources/AYON.ico create mode 100644 common/ayon_common/resources/AYON.png create mode 100644 common/ayon_common/resources/__init__.py create mode 100644 common/ayon_common/resources/edit.png create mode 100644 common/ayon_common/resources/eye.png create mode 100644 common/ayon_common/resources/stylesheet.css create mode 100644 common/ayon_common/utils.py delete mode 100644 common/openpype_common/distribution/addon_distribution.py delete mode 100644 common/openpype_common/distribution/addon_info.py create mode 100644 openpype/client/mongo/__init__.py create mode 100644 openpype/client/mongo/entities.py create mode 100644 openpype/client/mongo/entity_links.py rename openpype/client/{ => mongo}/mongo.py (98%) create mode 100644 openpype/client/mongo/operations.py create mode 100644 openpype/client/operations_base.py create mode 100644 openpype/client/server/__init__.py create mode 100644 openpype/client/server/constants.py create mode 100644 openpype/client/server/conversion_utils.py create mode 100644 openpype/client/server/entities.py create mode 100644 openpype/client/server/entity_links.py create mode 100644 openpype/client/server/openpype_comp.py create mode 100644 openpype/client/server/operations.py create mode 100644 openpype/client/server/utils.py rename openpype/{ => modules/sync_server}/plugins/load/add_site.py (100%) rename openpype/{ => modules/sync_server}/plugins/load/remove_site.py (100%) create mode 100644 openpype/plugins/publish/extract_hierarchy_to_ayon.py create mode 100644 openpype/plugins/publish/integrate_thumbnail_ayon.py create mode 100644 openpype/resources/icons/AYON_icon.png create mode 100644 openpype/resources/icons/AYON_splash.png create mode 100644 openpype/settings/ayon_settings.py create mode 100644 tools/run_tray_ayon.ps1 diff --git a/ayon_start.py b/ayon_start.py new file mode 100644 index 0000000000..11677b4415 --- /dev/null +++ b/ayon_start.py @@ -0,0 +1,376 @@ +# -*- coding: utf-8 -*- +"""Main entry point for AYON command. + +Bootstrapping process of AYON. +""" +import os +import sys +import site +import traceback +import contextlib + +# Enabled logging debug mode when "--debug" is passed +if "--verbose" in sys.argv: + expected_values = ( + "Expected: notset, debug, info, warning, error, critical" + " or integer [0-50]." + ) + idx = sys.argv.index("--verbose") + sys.argv.pop(idx) + if idx < len(sys.argv): + value = sys.argv.pop(idx) + else: + raise RuntimeError(( + f"Expect value after \"--verbose\" argument. {expected_values}" + )) + + log_level = None + low_value = value.lower() + if low_value.isdigit(): + log_level = int(low_value) + elif low_value == "notset": + log_level = 0 + elif low_value == "debug": + log_level = 10 + elif low_value == "info": + log_level = 20 + elif low_value == "warning": + log_level = 30 + elif low_value == "error": + log_level = 40 + elif low_value == "critical": + log_level = 50 + + if log_level is None: + raise ValueError(( + "Unexpected value after \"--verbose\" " + f"argument \"{value}\". {expected_values}" + )) + + os.environ["OPENPYPE_LOG_LEVEL"] = str(log_level) + +# Enable debug mode, may affect log level if log level is not defined +if "--debug" in sys.argv: + sys.argv.remove("--debug") + os.environ["OPENPYPE_DEBUG"] = "1" + +if "--automatic-tests" in sys.argv: + sys.argv.remove("--automatic-tests") + os.environ["IS_TEST"] = "1" + +if "--use-staging" in sys.argv: + sys.argv.remove("--use-staging") + os.environ["OPENPYPE_USE_STAGING"] = "1" + +_silent_commands = { + "run", + "standalonepublisher", + "extractenvironments", + "version" +} +if "--headless" in sys.argv: + os.environ["OPENPYPE_HEADLESS_MODE"] = "1" + sys.argv.remove("--headless") +elif os.getenv("OPENPYPE_HEADLESS_MODE") != "1": + os.environ.pop("OPENPYPE_HEADLESS_MODE", None) + + +IS_BUILT_APPLICATION = getattr(sys, "frozen", False) +HEADLESS_MODE_ENABLED = os.environ.get("OPENPYPE_HEADLESS_MODE") == "1" +SILENT_MODE_ENABLED = any(arg in _silent_commands for arg in sys.argv) + +_pythonpath = os.getenv("PYTHONPATH", "") +_python_paths = _pythonpath.split(os.pathsep) +if not IS_BUILT_APPLICATION: + # Code root defined by `start.py` directory + AYON_ROOT = os.path.dirname(os.path.abspath(__file__)) + _dependencies_path = site.getsitepackages()[-1] +else: + AYON_ROOT = os.path.dirname(sys.executable) + + # add dependencies folder to sys.pat for frozen code + _dependencies_path = os.path.normpath( + os.path.join(AYON_ROOT, "dependencies") + ) +# add stuff from `/dependencies` to PYTHONPATH. +sys.path.append(_dependencies_path) +_python_paths.append(_dependencies_path) + +# Vendored python modules that must not be in PYTHONPATH environment but +# are required for OpenPype processes +sys.path.insert(0, os.path.join(AYON_ROOT, "vendor", "python")) + +# Add common package to sys path +# - common contains common code for bootstraping and OpenPype processes +sys.path.insert(0, os.path.join(AYON_ROOT, "common")) + +# This is content of 'core' addon which is ATM part of build +common_python_vendor = os.path.join( + AYON_ROOT, + "openpype", + "vendor", + "python", + "common" +) +# Add tools dir to sys path for pyblish UI discovery +tools_dir = os.path.join(AYON_ROOT, "openpype", "tools") +for path in (AYON_ROOT, common_python_vendor, tools_dir): + while path in _python_paths: + _python_paths.remove(path) + + while path in sys.path: + sys.path.remove(path) + + _python_paths.insert(0, path) + sys.path.insert(0, path) + +os.environ["PYTHONPATH"] = os.pathsep.join(_python_paths) + +# enabled AYON state +os.environ["USE_AYON_SERVER"] = "1" +# Set this to point either to `python` from venv in case of live code +# or to `ayon` or `ayon_console` in case of frozen code +os.environ["OPENPYPE_EXECUTABLE"] = sys.executable +os.environ["AYON_ROOT"] = AYON_ROOT +os.environ["OPENPYPE_ROOT"] = AYON_ROOT +os.environ["OPENPYPE_REPOS_ROOT"] = AYON_ROOT +os.environ["AVALON_LABEL"] = "AYON" +# Set name of pyblish UI import +os.environ["PYBLISH_GUI"] = "pyblish_pype" + +import blessed # noqa: E402 +import certifi # noqa: E402 + + +if sys.__stdout__: + term = blessed.Terminal() + + def _print(message: str): + if SILENT_MODE_ENABLED: + pass + elif message.startswith("!!! "): + print(f'{term.orangered2("!!! ")}{message[4:]}') + elif message.startswith(">>> "): + print(f'{term.aquamarine3(">>> ")}{message[4:]}') + elif message.startswith("--- "): + print(f'{term.darkolivegreen3("--- ")}{message[4:]}') + elif message.startswith("*** "): + print(f'{term.gold("*** ")}{message[4:]}') + elif message.startswith(" - "): + print(f'{term.wheat(" - ")}{message[4:]}') + elif message.startswith(" . "): + print(f'{term.tan(" . ")}{message[4:]}') + elif message.startswith(" - "): + print(f'{term.seagreen3(" - ")}{message[7:]}') + elif message.startswith(" ! "): + print(f'{term.goldenrod(" ! ")}{message[7:]}') + elif message.startswith(" * "): + print(f'{term.aquamarine1(" * ")}{message[7:]}') + elif message.startswith(" "): + print(f'{term.darkseagreen3(" ")}{message[4:]}') + else: + print(message) +else: + def _print(message: str): + if not SILENT_MODE_ENABLED: + print(message) + + +# if SSL_CERT_FILE is not set prior to OpenPype launch, we set it to point +# to certifi bundle to make sure we have reasonably new CA certificates. +if not os.getenv("SSL_CERT_FILE"): + os.environ["SSL_CERT_FILE"] = certifi.where() +elif os.getenv("SSL_CERT_FILE") != certifi.where(): + _print("--- your system is set to use custom CA certificate bundle.") + +from ayon_common.connection.credentials import ( + ask_to_login_ui, + add_server, + need_server_or_login, + load_environments, + set_environments, + create_global_connection, + confirm_server_login, +) +from ayon_common.distribution.addon_distribution import AyonDistribution + + +def set_global_environments() -> None: + """Set global OpenPype's environments.""" + import acre + + from openpype.settings import get_general_environments + + general_env = get_general_environments() + + # first resolve general environment because merge doesn't expect + # values to be list. + # TODO: switch to OpenPype environment functions + merged_env = acre.merge( + acre.compute(acre.parse(general_env), cleanup=False), + dict(os.environ) + ) + env = acre.compute( + merged_env, + cleanup=False + ) + os.environ.clear() + os.environ.update(env) + + # Hardcoded default values + os.environ["PYBLISH_GUI"] = "pyblish_pype" + # Change scale factor only if is not set + if "QT_AUTO_SCREEN_SCALE_FACTOR" not in os.environ: + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" + + +def set_addons_environments(): + """Set global environments for OpenPype modules. + + This requires to have OpenPype in `sys.path`. + """ + + import acre + from openpype.modules import ModulesManager + + modules_manager = ModulesManager() + + # Merge environments with current environments and update values + if module_envs := modules_manager.collect_global_environments(): + parsed_envs = acre.parse(module_envs) + env = acre.merge(parsed_envs, dict(os.environ)) + os.environ.clear() + os.environ.update(env) + + +def _connect_to_ayon_server(): + load_environments() + if not need_server_or_login(): + create_global_connection() + return + + if HEADLESS_MODE_ENABLED: + _print("!!! Cannot open v4 Login dialog in headless mode.") + _print(( + "!!! Please use `AYON_SERVER_URL` to specify server address" + " and 'AYON_TOKEN' to specify user's token." + )) + sys.exit(1) + + current_url = os.environ.get("AYON_SERVER_URL") + url, token, username = ask_to_login_ui(current_url, always_on_top=True) + if url is not None and token is not None: + confirm_server_login(url, token, username) + return + + if url is not None: + add_server(url, username) + + _print("!!! Login was not successful.") + sys.exit(0) + + +def _check_and_update_from_ayon_server(): + """Gets addon info from v4, compares with local folder and updates it. + + Raises: + RuntimeError + """ + + distribution = AyonDistribution() + distribution.distribute() + distribution.validate_distribution() + + python_paths = [ + path + for path in os.getenv("PYTHONPATH", "").split(os.pathsep) + if path + ] + + for path in distribution.get_sys_paths(): + sys.path.insert(0, path) + if path not in python_paths: + python_paths.append(path) + os.environ["PYTHONPATH"] = os.pathsep.join(python_paths) + + +def boot(): + """Bootstrap OpenPype.""" + + from openpype.version import __version__ + + # TODO load version + os.environ["OPENPYPE_VERSION"] = __version__ + os.environ["AYON_VERSION"] = __version__ + + use_staging = os.environ.get("OPENPYPE_USE_STAGING") == "1" + + _connect_to_ayon_server() + _check_and_update_from_ayon_server() + + # delete OpenPype module and it's submodules from cache so it is used from + # specific version + modules_to_del = [ + sys.modules.pop(module_name) + for module_name in tuple(sys.modules) + if module_name == "openpype" or module_name.startswith("openpype.") + ] + + for module_name in modules_to_del: + with contextlib.suppress(AttributeError, KeyError): + del sys.modules[module_name] + + from openpype import cli + from openpype.lib import terminal as t + + _print(">>> loading environments ...") + _print(" - global AYON ...") + set_global_environments() + _print(" - for addons ...") + set_addons_environments() + + # print info when not running scripts defined in 'silent commands' + if not SILENT_MODE_ENABLED: + info = get_info(use_staging) + info.insert(0, f">>> Using AYON from [ {AYON_ROOT} ]") + + t_width = 20 + with contextlib.suppress(ValueError, OSError): + t_width = os.get_terminal_size().columns - 2 + + _header = f"*** AYON [{__version__}] " + info.insert(0, _header + "-" * (t_width - len(_header))) + + for i in info: + t.echo(i) + + try: + cli.main(obj={}, prog_name="openpype") + except Exception: # noqa + exc_info = sys.exc_info() + _print("!!! OpenPype crashed:") + traceback.print_exception(*exc_info) + sys.exit(1) + + +def get_info(use_staging=None) -> list: + """Print additional information to console.""" + + inf = [] + if use_staging: + inf.append(("AYON variant", "staging")) + else: + inf.append(("AYON variant", "production")) + + # NOTE add addons information + + maximum = max(len(i[0]) for i in inf) + formatted = [] + for info in inf: + padding = (maximum - len(info[0])) + 1 + formatted.append(f'... {info[0]}:{" " * padding}[ {info[1]} ]') + return formatted + + +if __name__ == "__main__": + boot() diff --git a/common/openpype_common/distribution/__init__.py b/common/ayon_common/connection/__init__.py similarity index 100% rename from common/openpype_common/distribution/__init__.py rename to common/ayon_common/connection/__init__.py diff --git a/common/ayon_common/connection/credentials.py b/common/ayon_common/connection/credentials.py new file mode 100644 index 0000000000..13d8fe2d7d --- /dev/null +++ b/common/ayon_common/connection/credentials.py @@ -0,0 +1,475 @@ +"""Handle credentials and connection to server for client application. + +Cache and store used server urls. Store/load API keys to/from keyring if +needed. Store metadata about used urls, usernames for the urls and when was +the connection with the username established. + +On bootstrap is created global connection with information about site and +client version. The connection object lives in 'ayon_api'. +""" + +import os +import json +import platform +import datetime +import contextlib +from typing import Optional, Union, Any + +import ayon_api + +from ayon_api.exceptions import UrlError +from ayon_api.utils import ( + validate_url, + is_token_valid, + logout_from_server, +) + +from ayon_common.utils import get_ayon_appdirs, get_local_site_id + + +class ChangeUserResult: + def __init__( + self, logged_out, old_url, old_token, old_username, + new_url, new_token, new_username + ): + shutdown = logged_out + restart = new_url is not None and new_url != old_url + token_changed = new_token is not None and new_token == old_token + + self.logged_out = logged_out + self.old_url = old_url + self.old_token = old_token + self.old_username = old_username + self.new_url = new_url + self.new_token = new_token + self.new_username = new_username + + self.shutdown = shutdown + self.restart = restart + self.token_changed = token_changed + + +def _get_servers_path(): + return get_ayon_appdirs("used_servers.json") + + +def get_servers_info_data(): + """Metadata about used server on this machine. + + Store data about all used server urls, last used url and user username for + the url. Using this metadata we can remember which username was used per + url if token stored in keyring loose lifetime. + + Returns: + dict[str, Any]: Information about servers. + """ + + data = {} + servers_info_path = _get_servers_path() + if not os.path.exists(servers_info_path): + dirpath = os.path.dirname(servers_info_path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + return data + + with open(servers_info_path, "r") as stream: + with contextlib.suppress(BaseException): + data = json.load(stream) + return data + + +def add_server(url: str, username: str): + """Add server to server info metadata. + + This function will also mark the url as last used url on the machine so on + next launch will be used. + + Args: + url (str): Server url. + username (str): Name of user used to log in. + """ + + servers_info_path = _get_servers_path() + data = get_servers_info_data() + data["last_server"] = url + if "urls" not in data: + data["urls"] = {} + data["urls"][url] = { + "updated_dt": datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"), + "username": username, + } + + with open(servers_info_path, "w") as stream: + json.dump(data, stream) + + +def remove_server(url: str): + """Remove server url from servers information. + + This should be used on logout to completelly loose information about server + on the machine. + + Args: + url (str): Server url. + """ + + if not url: + return + + servers_info_path = _get_servers_path() + data = get_servers_info_data() + if data.get("last_server") == url: + data["last_server"] = None + + if "urls" in data: + data["urls"].pop(url, None) + + with open(servers_info_path, "w") as stream: + json.dump(data, stream) + + +def get_last_server( + data: Optional[dict[str, Any]] = None +) -> Union[str, None]: + """Last server used to log in on this machine. + + Args: + data (Optional[dict[str, Any]]): Prepared server information data. + + Returns: + Union[str, None]: Last used server url. + """ + + if data is None: + data = get_servers_info_data() + return data.get("last_server") + + +def get_last_username_by_url( + url: str, + data: Optional[dict[str, Any]] = None +) -> Union[str, None]: + """Get last username which was used for passed url. + + Args: + url (str): Server url. + data (Optional[dict[str, Any]]): Servers info. + + Returns: + Union[str, None]: Username. + """ + + if not url: + return None + + if data is None: + data = get_servers_info_data() + + if urls := data.get("urls"): + if url_info := urls.get(url): + return url_info.get("username") + return None + + +def get_last_server_with_username(): + """Receive last server and username used in last connection. + + Returns: + tuple[Union[str, None], Union[str, None]]: Url and username. + """ + + data = get_servers_info_data() + url = get_last_server(data) + username = get_last_username_by_url(url) + return url, username + + +class TokenKeyring: + # Fake username with hardcoded username + username_key = "username" + + def __init__(self, url): + try: + import keyring + + except Exception as exc: + raise NotImplementedError( + "Python module `keyring` is not available." + ) from exc + + # hack for cx_freeze and Windows keyring backend + if platform.system().lower() == "windows": + from keyring.backends import Windows + + keyring.set_keyring(Windows.WinVaultKeyring()) + + self._url = url + self._keyring_key = f"AYON/{url}" + + def get_value(self): + import keyring + + return keyring.get_password(self._keyring_key, self.username_key) + + def set_value(self, value): + import keyring + + if value is not None: + keyring.set_password(self._keyring_key, self.username_key, value) + return + + with contextlib.suppress(keyring.errors.PasswordDeleteError): + keyring.delete_password(self._keyring_key, self.username_key) + + +def load_token(url: str) -> Union[str, None]: + """Get token for url from keyring. + + Args: + url (str): Server url. + + Returns: + Union[str, None]: Token for passed url available in keyring. + """ + + return TokenKeyring(url).get_value() + + +def store_token(url: str, token: str): + """Store token by url to keyring. + + Args: + url (str): Server url. + token (str): User token to server. + """ + + TokenKeyring(url).set_value(token) + + +def ask_to_login_ui( + url: Optional[str] = None, + always_on_top: Optional[bool] = False +) -> tuple[str, str, str]: + """Ask user to login using UI. + + This should be used only when user is not yet logged in at all or available + credentials are invalid. To change credentials use 'change_user_ui' + function. + + Args: + url (Optional[str]): Server url that could be prefilled in UI. + always_on_top (Optional[bool]): Window will be drawn on top of + other windows. + + Returns: + tuple[str, str, str]: Url, user's token and username. + """ + + from .ui import ask_to_login + + if url is None: + url = get_last_server() + username = get_last_username_by_url(url) + return ask_to_login(url, username, always_on_top=always_on_top) + + +def change_user_ui() -> ChangeUserResult: + """Change user using UI. + + Show UI to user where he can change credentials or url. Output will contain + all information about old/new values of url, username, api key. If user + confirmed or declined values. + + Returns: + ChangeUserResult: Information about user change. + """ + + from .ui import change_user + + url, username = get_last_server_with_username() + token = load_token(url) + result = change_user(url, username, token) + new_url, new_token, new_username, logged_out = result + + output = ChangeUserResult( + logged_out, url, token, username, + new_url, new_token, new_username + ) + if output.logged_out: + logout(url, token) + + elif output.token_changed: + change_token( + output.new_url, + output.new_token, + output.new_username, + output.old_url + ) + return output + + +def change_token( + url: str, + token: str, + username: Optional[str] = None, + old_url: Optional[str] = None +): + """Change url and token in currently running session. + + Function can also change server url, in that case are previous credentials + NOT removed from cache. + + Args: + url (str): Url to server. + token (str): New token to be used for url connection. + username (Optional[str]): Username of logged user. + old_url (Optional[str]): Previous url. Value from 'get_last_server' + is used if not entered. + """ + + if old_url is None: + old_url = get_last_server() + if old_url and old_url == url: + remove_url_cache(old_url) + + # TODO check if ayon_api is already connected + add_server(url, username) + store_token(url, token) + ayon_api.change_token(url, token) + + +def remove_url_cache(url: str): + """Clear cache for server url. + + Args: + url (str): Server url which is removed from cache. + """ + + store_token(url, None) + + +def remove_token_cache(url: str, token: str): + """Remove token from local cache of url. + + Is skipped if cached token under the passed url is not the same + as passed token. + + Args: + url (str): Url to server. + token (str): Token to be removed from url cache. + """ + + if load_token(url) == token: + remove_url_cache(url) + + +def logout(url: str, token: str): + """Logout from server and throw token away. + + Args: + url (str): Url from which should be logged out. + token (str): Token which should be used to log out. + """ + + remove_server(url) + ayon_api.close_connection() + ayon_api.set_environments(None, None) + remove_token_cache(url, token) + logout_from_server(url, token) + + +def load_environments(): + """Load environments on startup. + + Handle environments needed for connection with server. Environments are + 'AYON_SERVER_URL' and 'AYON_TOKEN'. + + Server is looked up from environment. Already set environent is not + changed. If environemnt is not filled then last server stored in appdirs + is used. + + Token is skipped if url is not available. Otherwise, is also checked from + env and if is not available then uses 'load_token' to try to get token + based on server url. + """ + + server_url = os.environ.get("AYON_SERVER_URL") + if not server_url: + server_url = get_last_server() + if not server_url: + return + os.environ["AYON_SERVER_URL"] = server_url + + if not os.environ.get("AYON_TOKEN"): + if token := load_token(server_url): + os.environ["AYON_TOKEN"] = token + + +def set_environments(url: str, token: str): + """Change url and token environemnts in currently running process. + + Args: + url (str): New server url. + token (str): User's token. + """ + + ayon_api.set_environments(url, token) + + +def create_global_connection(): + """Create global connection with site id and client version. + + + Make sure the global connection in 'ayon_api' have entered site id and + client version. + """ + + if hasattr(ayon_api, "create_connection"): + ayon_api.create_connection( + get_local_site_id(), os.environ.get("AYON_VERSION") + ) + + +def need_server_or_login() -> bool: + """Check if server url or login to the server are needed. + + It is recommended to call 'load_environments' on startup before this check. + But in some cases this function could be called after startup. + + Returns: + bool: 'True' if server and token are available. Otherwise 'False'. + """ + + server_url = os.environ.get("AYON_SERVER_URL") + if not server_url: + return True + + try: + server_url = validate_url(server_url) + except UrlError: + return True + + token = os.environ.get("AYON_TOKEN") + if token: + return not is_token_valid(server_url, token) + + token = load_token(server_url) + return not is_token_valid(server_url, token) + + +def confirm_server_login(url, token, username): + """Confirm login of user and do necessary stepts to apply changes. + + This should not be used on "change" of user but on first login. + + Args: + url (str): Server url where user authenticated. + token (str): API token used for authentication to server. + username (Union[str, None]): Username related to API token. + """ + + add_server(url, username) + store_token(url, token) + set_environments(url, token) + create_global_connection() diff --git a/common/ayon_common/connection/ui/__init__.py b/common/ayon_common/connection/ui/__init__.py new file mode 100644 index 0000000000..96e573df0d --- /dev/null +++ b/common/ayon_common/connection/ui/__init__.py @@ -0,0 +1,12 @@ +from .login_window import ( + ServerLoginWindow, + ask_to_login, + change_user, +) + + +__all__ = ( + "ServerLoginWindow", + "ask_to_login", + "change_user", +) diff --git a/common/ayon_common/connection/ui/lib.py b/common/ayon_common/connection/ui/lib.py new file mode 100644 index 0000000000..e0f0a3d6c2 --- /dev/null +++ b/common/ayon_common/connection/ui/lib.py @@ -0,0 +1,11 @@ +def set_style_property(widget, property_name, property_value): + """Set widget's property that may affect style. + + Style of widget is polished if current property value is different. + """ + + cur_value = widget.property(property_name) + if cur_value == property_value: + return + widget.setProperty(property_name, property_value) + widget.style().polish(widget) diff --git a/common/ayon_common/connection/ui/login_window.py b/common/ayon_common/connection/ui/login_window.py new file mode 100644 index 0000000000..f2604f0466 --- /dev/null +++ b/common/ayon_common/connection/ui/login_window.py @@ -0,0 +1,753 @@ +import traceback + +from Qt import QtWidgets, QtCore, QtGui + +from ayon_api.exceptions import UrlError +from ayon_api.utils import validate_url, login_to_server + +from ayon_common.resources import ( + get_resource_path, + get_icon_path, + load_stylesheet, +) + +from .widgets import ( + PressHoverButton, + PlaceholderLineEdit, +) +from .lib import set_style_property + + +class LogoutConfirmDialog(QtWidgets.QDialog): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.setWindowTitle("Logout confirmation") + + message_widget = QtWidgets.QWidget(self) + + message_label = QtWidgets.QLabel( + ( + "You are going to logout. This action will close this" + " application and will invalidate your login." + " All other applications launched with this login won't be" + " able to use it anymore.

" + "You can cancel logout and only change server and user login" + " in login dialog.

" + "Press OK to confirm logout." + ), + message_widget + ) + message_label.setWordWrap(True) + + message_layout = QtWidgets.QHBoxLayout(message_widget) + message_layout.setContentsMargins(0, 0, 0, 0) + message_layout.addWidget(message_label, 1) + + sep_frame = QtWidgets.QFrame(self) + sep_frame.setObjectName("Separator") + sep_frame.setMinimumHeight(2) + sep_frame.setMaximumHeight(2) + + footer_widget = QtWidgets.QWidget(self) + + cancel_btn = QtWidgets.QPushButton("Cancel", footer_widget) + confirm_btn = QtWidgets.QPushButton("OK", footer_widget) + + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addStretch(1) + footer_layout.addWidget(cancel_btn, 0) + footer_layout.addWidget(confirm_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(message_widget, 0) + main_layout.addStretch(1) + main_layout.addWidget(sep_frame, 0) + main_layout.addWidget(footer_widget, 0) + + cancel_btn.clicked.connect(self._on_cancel_click) + confirm_btn.clicked.connect(self._on_confirm_click) + + self._cancel_btn = cancel_btn + self._confirm_btn = confirm_btn + self._result = False + + def showEvent(self, event): + super().showEvent(event) + self._match_btns_sizes() + + def resizeEvent(self, event): + super().resizeEvent(event) + self._match_btns_sizes() + + def _match_btns_sizes(self): + width = max( + self._cancel_btn.sizeHint().width(), + self._confirm_btn.sizeHint().width() + ) + self._cancel_btn.setMinimumWidth(width) + self._confirm_btn.setMinimumWidth(width) + + def _on_cancel_click(self): + self._result = False + self.reject() + + def _on_confirm_click(self): + self._result = True + self.accept() + + def get_result(self): + return self._result + + +class ServerLoginWindow(QtWidgets.QDialog): + default_width = 410 + default_height = 170 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + icon_path = get_icon_path() + icon = QtGui.QIcon(icon_path) + self.setWindowIcon(icon) + self.setWindowTitle("Login to server") + + edit_icon_path = get_resource_path("edit.png") + edit_icon = QtGui.QIcon(edit_icon_path) + + # --- URL page --- + login_widget = QtWidgets.QWidget(self) + + user_cred_widget = QtWidgets.QWidget(login_widget) + + url_label = QtWidgets.QLabel("URL:", user_cred_widget) + + url_widget = QtWidgets.QWidget(user_cred_widget) + + url_input = PlaceholderLineEdit(url_widget) + url_input.setPlaceholderText("< https://ayon.server.com >") + + url_preview = QtWidgets.QLineEdit(url_widget) + url_preview.setReadOnly(True) + url_preview.setObjectName("LikeDisabledInput") + + url_edit_btn = PressHoverButton(user_cred_widget) + url_edit_btn.setIcon(edit_icon) + url_edit_btn.setObjectName("PasswordBtn") + + url_layout = QtWidgets.QHBoxLayout(url_widget) + url_layout.setContentsMargins(0, 0, 0, 0) + url_layout.addWidget(url_input, 1) + url_layout.addWidget(url_preview, 1) + + # --- URL separator --- + url_cred_sep = QtWidgets.QFrame(self) + url_cred_sep.setObjectName("Separator") + url_cred_sep.setMinimumHeight(2) + url_cred_sep.setMaximumHeight(2) + + # --- Login page --- + username_label = QtWidgets.QLabel("Username:", user_cred_widget) + + username_widget = QtWidgets.QWidget(user_cred_widget) + + username_input = PlaceholderLineEdit(username_widget) + username_input.setPlaceholderText("< Artist >") + + username_preview = QtWidgets.QLineEdit(username_widget) + username_preview.setReadOnly(True) + username_preview.setObjectName("LikeDisabledInput") + + username_edit_btn = PressHoverButton(user_cred_widget) + username_edit_btn.setIcon(edit_icon) + username_edit_btn.setObjectName("PasswordBtn") + + username_layout = QtWidgets.QHBoxLayout(username_widget) + username_layout.setContentsMargins(0, 0, 0, 0) + username_layout.addWidget(username_input, 1) + username_layout.addWidget(username_preview, 1) + + password_label = QtWidgets.QLabel("Password:", user_cred_widget) + password_input = PlaceholderLineEdit(user_cred_widget) + password_input.setPlaceholderText("< *********** >") + password_input.setEchoMode(password_input.Password) + + api_label = QtWidgets.QLabel("API key:", user_cred_widget) + api_preview = QtWidgets.QLineEdit(user_cred_widget) + api_preview.setReadOnly(True) + api_preview.setObjectName("LikeDisabledInput") + + show_password_icon_path = get_resource_path("eye.png") + show_password_icon = QtGui.QIcon(show_password_icon_path) + show_password_btn = PressHoverButton(user_cred_widget) + show_password_btn.setObjectName("PasswordBtn") + show_password_btn.setIcon(show_password_icon) + show_password_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + + cred_msg_sep = QtWidgets.QFrame(self) + cred_msg_sep.setObjectName("Separator") + cred_msg_sep.setMinimumHeight(2) + cred_msg_sep.setMaximumHeight(2) + + # --- Credentials inputs --- + user_cred_layout = QtWidgets.QGridLayout(user_cred_widget) + user_cred_layout.setContentsMargins(0, 0, 0, 0) + row = 0 + + user_cred_layout.addWidget(url_label, row, 0, 1, 1) + user_cred_layout.addWidget(url_widget, row, 1, 1, 1) + user_cred_layout.addWidget(url_edit_btn, row, 2, 1, 1) + row += 1 + + user_cred_layout.addWidget(url_cred_sep, row, 0, 1, 3) + row += 1 + + user_cred_layout.addWidget(username_label, row, 0, 1, 1) + user_cred_layout.addWidget(username_widget, row, 1, 1, 1) + user_cred_layout.addWidget(username_edit_btn, row, 2, 2, 1) + row += 1 + + user_cred_layout.addWidget(api_label, row, 0, 1, 1) + user_cred_layout.addWidget(api_preview, row, 1, 1, 1) + row += 1 + + user_cred_layout.addWidget(password_label, row, 0, 1, 1) + user_cred_layout.addWidget(password_input, row, 1, 1, 1) + user_cred_layout.addWidget(show_password_btn, row, 2, 1, 1) + row += 1 + + user_cred_layout.addWidget(cred_msg_sep, row, 0, 1, 3) + row += 1 + + user_cred_layout.setColumnStretch(0, 0) + user_cred_layout.setColumnStretch(1, 1) + user_cred_layout.setColumnStretch(2, 0) + + login_layout = QtWidgets.QVBoxLayout(login_widget) + login_layout.setContentsMargins(0, 0, 0, 0) + login_layout.addWidget(user_cred_widget, 1) + + # --- Messages --- + # Messages for users (e.g. invalid url etc.) + message_label = QtWidgets.QLabel(self) + message_label.setWordWrap(True) + message_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + + footer_widget = QtWidgets.QWidget(self) + logout_btn = QtWidgets.QPushButton("Logout", footer_widget) + user_message = QtWidgets.QLabel(footer_widget) + login_btn = QtWidgets.QPushButton("Login", footer_widget) + confirm_btn = QtWidgets.QPushButton("Confirm", footer_widget) + + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addWidget(logout_btn, 0) + footer_layout.addWidget(user_message, 1) + footer_layout.addWidget(login_btn, 0) + footer_layout.addWidget(confirm_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(login_widget, 0) + main_layout.addWidget(message_label, 0) + main_layout.addStretch(1) + main_layout.addWidget(footer_widget, 0) + + url_input.textChanged.connect(self._on_url_change) + url_input.returnPressed.connect(self._on_url_enter_press) + username_input.textChanged.connect(self._on_user_change) + username_input.returnPressed.connect(self._on_username_enter_press) + password_input.returnPressed.connect(self._on_password_enter_press) + show_password_btn.change_state.connect(self._on_show_password) + url_edit_btn.clicked.connect(self._on_url_edit_click) + username_edit_btn.clicked.connect(self._on_username_edit_click) + logout_btn.clicked.connect(self._on_logout_click) + login_btn.clicked.connect(self._on_login_click) + confirm_btn.clicked.connect(self._on_login_click) + + self._message_label = message_label + + self._url_widget = url_widget + self._url_input = url_input + self._url_preview = url_preview + self._url_edit_btn = url_edit_btn + + self._login_widget = login_widget + + self._user_cred_widget = user_cred_widget + self._username_input = username_input + self._username_preview = username_preview + self._username_edit_btn = username_edit_btn + + self._password_label = password_label + self._password_input = password_input + self._show_password_btn = show_password_btn + self._api_label = api_label + self._api_preview = api_preview + + self._logout_btn = logout_btn + self._user_message = user_message + self._login_btn = login_btn + self._confirm_btn = confirm_btn + + self._url_is_valid = None + self._credentials_are_valid = None + self._result = (None, None, None, False) + self._first_show = True + + self._allow_logout = False + self._logged_in = False + self._url_edit_mode = False + self._username_edit_mode = False + + def set_allow_logout(self, allow_logout): + if allow_logout is self._allow_logout: + return + self._allow_logout = allow_logout + + self._update_states_by_edit_mode() + + def _set_logged_in(self, logged_in): + if logged_in is self._logged_in: + return + self._logged_in = logged_in + + self._update_states_by_edit_mode() + + def _set_url_edit_mode(self, edit_mode): + if self._url_edit_mode is not edit_mode: + self._url_edit_mode = edit_mode + self._update_states_by_edit_mode() + + def _set_username_edit_mode(self, edit_mode): + if self._username_edit_mode is not edit_mode: + self._username_edit_mode = edit_mode + self._update_states_by_edit_mode() + + def _get_url_user_edit(self): + url_edit = True + if self._logged_in and not self._url_edit_mode: + url_edit = False + user_edit = url_edit + if not user_edit and self._logged_in and self._username_edit_mode: + user_edit = True + return url_edit, user_edit + + def _update_states_by_edit_mode(self): + url_edit, user_edit = self._get_url_user_edit() + + self._url_preview.setVisible(not url_edit) + self._url_input.setVisible(url_edit) + self._url_edit_btn.setVisible(self._allow_logout and not url_edit) + + self._username_preview.setVisible(not user_edit) + self._username_input.setVisible(user_edit) + self._username_edit_btn.setVisible( + self._allow_logout and not user_edit + ) + + self._api_preview.setVisible(not user_edit) + self._api_label.setVisible(not user_edit) + + self._password_label.setVisible(user_edit) + self._show_password_btn.setVisible(user_edit) + self._password_input.setVisible(user_edit) + + self._logout_btn.setVisible(self._allow_logout and self._logged_in) + self._login_btn.setVisible(not self._allow_logout) + self._confirm_btn.setVisible(self._allow_logout) + self._update_login_btn_state(url_edit, user_edit) + + def _update_login_btn_state(self, url_edit=None, user_edit=None, url=None): + if url_edit is None: + url_edit, user_edit = self._get_url_user_edit() + + if url is None: + url = self._url_input.text() + + enabled = bool(url) and (url_edit or user_edit) + + self._login_btn.setEnabled(enabled) + self._confirm_btn.setEnabled(enabled) + + def showEvent(self, event): + super().showEvent(event) + if self._first_show: + self._first_show = False + self._on_first_show() + + def _on_first_show(self): + self.setStyleSheet(load_stylesheet()) + self.resize(self.default_width, self.default_height) + self._center_window() + if self._allow_logout is None: + self.set_allow_logout(False) + + self._update_states_by_edit_mode() + if not self._url_input.text(): + widget = self._url_input + elif not self._username_input.text(): + widget = self._username_input + else: + widget = self._password_input + + self._set_input_focus(widget) + + def result(self): + """Result url and token or login. + + Returns: + Union[Tuple[str, str], Tuple[None, None]]: Url and token used for + login if was successful otherwise are both set to None. + """ + return self._result + + def _center_window(self): + """Move window to center of screen.""" + + desktop = QtWidgets.QApplication.desktop() + screen_idx = desktop.screenNumber(self) + screen_geo = desktop.screenGeometry(screen_idx) + geo = self.frameGeometry() + geo.moveCenter(screen_geo.center()) + if geo.y() < screen_geo.y(): + geo.setY(screen_geo.y()) + + self.move(geo.topLeft()) + + def _on_url_change(self, text): + self._update_login_btn_state(url=text) + self._set_url_valid(None) + self._set_credentials_valid(None) + self._url_preview.setText(text) + + def _set_url_valid(self, valid): + if valid is self._url_is_valid: + return + + self._url_is_valid = valid + self._set_input_valid_state(self._url_input, valid) + + def _set_credentials_valid(self, valid): + if self._credentials_are_valid is valid: + return + + self._credentials_are_valid = valid + self._set_input_valid_state(self._username_input, valid) + self._set_input_valid_state(self._password_input, valid) + + def _on_url_enter_press(self): + self._set_input_focus(self._username_input) + + def _on_user_change(self, username): + self._username_preview.setText(username) + + def _on_username_enter_press(self): + self._set_input_focus(self._password_input) + + def _on_password_enter_press(self): + self._login() + + def _on_show_password(self, show_password): + if show_password: + placeholder_text = "< MySecret124 >" + echo_mode = QtWidgets.QLineEdit.Normal + else: + placeholder_text = "< *********** >" + echo_mode = QtWidgets.QLineEdit.Password + + self._password_input.setEchoMode(echo_mode) + self._password_input.setPlaceholderText(placeholder_text) + + def _on_username_edit_click(self): + self._username_edit_mode = True + self._update_states_by_edit_mode() + + def _on_url_edit_click(self): + self._url_edit_mode = True + self._update_states_by_edit_mode() + + def _on_logout_click(self): + dialog = LogoutConfirmDialog(self) + dialog.exec_() + if dialog.get_result(): + self._result = (None, None, None, True) + self.accept() + + def _on_login_click(self): + self._login() + + def _validate_url(self): + """Use url from input to connect and change window state on success. + + Todos: + Threaded check. + """ + + url = self._url_input.text() + valid_url = None + try: + valid_url = validate_url(url) + + except UrlError as exc: + parts = [f"{exc.title}"] + parts.extend(f"- {hint}" for hint in exc.hints) + self._set_message("
".join(parts)) + + except KeyboardInterrupt: + # Reraise KeyboardInterrupt error + raise + + except BaseException: + self._set_unexpected_error() + return + + if valid_url is None: + return False + + self._url_input.setText(valid_url) + return True + + def _login(self): + if ( + not self._login_btn.isEnabled() + and not self._confirm_btn.isEnabled() + ): + return + + if not self._url_is_valid: + self._set_url_valid(self._validate_url()) + + if not self._url_is_valid: + self._set_input_focus(self._url_input) + self._set_credentials_valid(None) + return + + self._clear_message() + + url = self._url_input.text() + username = self._username_input.text() + password = self._password_input.text() + try: + token = login_to_server(url, username, password) + except BaseException: + self._set_unexpected_error() + return + + if token is not None: + self._result = (url, token, username, False) + self.accept() + return + + self._set_credentials_valid(False) + message_lines = ["Invalid credentials"] + if not username.strip(): + message_lines.append("- Username is not filled") + + if not password.strip(): + message_lines.append("- Password is not filled") + + if username and password: + message_lines.append("- Check your credentials") + + self._set_message("
".join(message_lines)) + self._set_input_focus(self._username_input) + + def _set_input_focus(self, widget): + widget.setFocus(QtCore.Qt.MouseFocusReason) + + def _set_input_valid_state(self, widget, valid): + state = "" + if valid is True: + state = "valid" + elif valid is False: + state = "invalid" + set_style_property(widget, "state", state) + + def _set_message(self, message): + self._message_label.setText(message) + + def _clear_message(self): + self._message_label.setText("") + + def _set_unexpected_error(self): + # TODO add traceback somewhere + # - maybe a button to show or copy? + traceback.print_exc() + lines = [ + "Unexpected error happened", + "- Can be caused by wrong url (leading elsewhere)" + ] + self._set_message("
".join(lines)) + + def set_url(self, url): + self._url_preview.setText(url) + self._url_input.setText(url) + self._validate_url() + + def set_username(self, username): + self._username_preview.setText(username) + self._username_input.setText(username) + + def _set_api_key(self, api_key): + if not api_key or len(api_key) < 3: + self._api_preview.setText(api_key or "") + return + + api_key_len = len(api_key) + offset = 6 + if api_key_len < offset: + offset = api_key_len // 2 + api_key = api_key[:offset] + "." * (api_key_len - offset) + + self._api_preview.setText(api_key) + + def set_logged_in( + self, + logged_in, + url=None, + username=None, + api_key=None, + allow_logout=None + ): + if url is not None: + self.set_url(url) + + if username is not None: + self.set_username(username) + + if api_key: + self._set_api_key(api_key) + + if logged_in and allow_logout is None: + allow_logout = True + + self._set_logged_in(logged_in) + + if allow_logout: + self.set_allow_logout(True) + elif allow_logout is False: + self.set_allow_logout(False) + + +def ask_to_login(url=None, username=None, always_on_top=False): + """Ask user to login using Qt dialog. + + Function creates new QApplication if is not created yet. + + Args: + url (Optional[str]): Server url that will be prefilled in dialog. + username (Optional[str]): Username that will be prefilled in dialog. + always_on_top (Optional[bool]): Window will be drawn on top of + other windows. + + Returns: + tuple[str, str, str]: Returns Url, user's token and username. Url can + be changed during dialog lifetime that's why the url is returned. + """ + + app_instance = QtWidgets.QApplication.instance() + if app_instance is None: + for attr_name in ( + "AA_EnableHighDpiScaling", + "AA_UseHighDpiPixmaps" + ): + attr = getattr(QtCore.Qt, attr_name, None) + if attr is not None: + QtWidgets.QApplication.setAttribute(attr) + app_instance = QtWidgets.QApplication([]) + + window = ServerLoginWindow() + if always_on_top: + window.setWindowFlags( + window.windowFlags() + | QtCore.Qt.WindowStaysOnTopHint + ) + + if url: + window.set_url(url) + + if username: + window.set_username(username) + + _output = {"out": None} + + def _exec_window(): + window.exec_() + result = window.result() + out_url, out_token, out_username, _logged_out = result + _output["out"] = out_url, out_token, out_username + return _output["out"] + + # Use QTimer to exec dialog if application is not running yet + # - it is not possible to call 'exec_' on dialog without running app + # - it is but the window is stuck + if not app_instance.startingUp(): + return _exec_window() + + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.timeout.connect(_exec_window) + timer.start() + # This can become main Qt loop. Maybe should live elsewhere + app_instance.exec_() + + return _output["out"] + + +def change_user(url, username, api_key, always_on_top=False): + """Ask user to login using Qt dialog. + + Function creates new QApplication if is not created yet. + + Args: + url (str): Server url that will be prefilled in dialog. + username (str): Username that will be prefilled in dialog. + api_key (str): API key that will be prefilled in dialog. + always_on_top (Optional[bool]): Window will be drawn on top of + other windows. + + Returns: + Tuple[str, str]: Returns Url and user's token. Url can be changed + during dialog lifetime that's why the url is returned. + """ + + app_instance = QtWidgets.QApplication.instance() + if app_instance is None: + for attr_name in ( + "AA_EnableHighDpiScaling", + "AA_UseHighDpiPixmaps" + ): + attr = getattr(QtCore.Qt, attr_name, None) + if attr is not None: + QtWidgets.QApplication.setAttribute(attr) + app_instance = QtWidgets.QApplication([]) + + window = ServerLoginWindow() + if always_on_top: + window.setWindowFlags( + window.windowFlags() + | QtCore.Qt.WindowStaysOnTopHint + ) + window.set_logged_in(True, url, username, api_key) + + _output = {"out": None} + + def _exec_window(): + window.exec_() + _output["out"] = window.result() + return _output["out"] + + # Use QTimer to exec dialog if application is not running yet + # - it is not possible to call 'exec_' on dialog without running app + # - it is but the window is stuck + if not app_instance.startingUp(): + return _exec_window() + + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.timeout.connect(_exec_window) + timer.start() + # This can become main Qt loop. Maybe should live elsewhere + app_instance.exec_() + return _output["out"] diff --git a/common/ayon_common/connection/ui/widgets.py b/common/ayon_common/connection/ui/widgets.py new file mode 100644 index 0000000000..04c6a8e5f2 --- /dev/null +++ b/common/ayon_common/connection/ui/widgets.py @@ -0,0 +1,47 @@ +from Qt import QtWidgets, QtCore, QtGui + + +class PressHoverButton(QtWidgets.QPushButton): + """Keep track about mouse press/release and enter/leave.""" + + _mouse_pressed = False + _mouse_hovered = False + change_state = QtCore.Signal(bool) + + def mousePressEvent(self, event): + self._mouse_pressed = True + self._mouse_hovered = True + self.change_state.emit(self._mouse_hovered) + super(PressHoverButton, self).mousePressEvent(event) + + def mouseReleaseEvent(self, event): + self._mouse_pressed = False + self._mouse_hovered = False + self.change_state.emit(self._mouse_hovered) + super(PressHoverButton, self).mouseReleaseEvent(event) + + def mouseMoveEvent(self, event): + mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos()) + under_mouse = self.rect().contains(mouse_pos) + if under_mouse != self._mouse_hovered: + self._mouse_hovered = under_mouse + self.change_state.emit(self._mouse_hovered) + + super(PressHoverButton, self).mouseMoveEvent(event) + + +class PlaceholderLineEdit(QtWidgets.QLineEdit): + """Set placeholder color of QLineEdit in Qt 5.12 and higher.""" + + def __init__(self, *args, **kwargs): + super(PlaceholderLineEdit, self).__init__(*args, **kwargs) + # Change placeholder palette color + if hasattr(QtGui.QPalette, "PlaceholderText"): + filter_palette = self.palette() + color = QtGui.QColor("#D3D8DE") + color.setAlpha(67) + filter_palette.setColor( + QtGui.QPalette.PlaceholderText, + color + ) + self.setPalette(filter_palette) diff --git a/common/openpype_common/distribution/README.md b/common/ayon_common/distribution/README.md similarity index 100% rename from common/openpype_common/distribution/README.md rename to common/ayon_common/distribution/README.md diff --git a/common/ayon_common/distribution/__init__.py b/common/ayon_common/distribution/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/ayon_common/distribution/addon_distribution.py b/common/ayon_common/distribution/addon_distribution.py new file mode 100644 index 0000000000..dba6c20193 --- /dev/null +++ b/common/ayon_common/distribution/addon_distribution.py @@ -0,0 +1,1189 @@ +import os +import sys +import json +import traceback +import collections +import datetime +from enum import Enum +from abc import abstractmethod +import attr +import logging +import platform +import shutil +import threading +from abc import ABCMeta + +import ayon_api + +from ayon_common.utils import get_ayon_appdirs +from .file_handler import RemoteFileHandler +from .addon_info import ( + AddonInfo, + UrlType, + DependencyItem, +) + + +class UpdateState(Enum): + UNKNOWN = "unknown" + UPDATED = "udated" + OUTDATED = "outdated" + UPDATE_FAILED = "failed" + MISS_SOURCE_FILES = "miss_source_files" + + +def get_local_dir(*subdirs): + """Get product directory in user's home directory. + + Each user on machine have own local directory where are downloaded updates, + addons etc. + + Returns: + str: Path to product local directory. + """ + + if not subdirs: + raise ValueError("Must fill dir_name if nothing else provided!") + + local_dir = get_ayon_appdirs(*subdirs) + if not os.path.isdir(local_dir): + try: + os.makedirs(local_dir) + except Exception: # TODO fix exception + raise RuntimeError(f"Cannot create {local_dir}") + + return local_dir + + +def get_addons_dir(): + """Directory where addon packages are stored. + + Path to addons is defined using python module 'appdirs' which + + The path is stored into environment variable 'AYON_ADDONS_DIR'. + Value of environment variable can be overriden, but we highly recommended + to use that option only for development purposes. + + Returns: + str: Path to directory where addons should be downloaded. + """ + + addons_dir = os.environ.get("AYON_ADDONS_DIR") + if not addons_dir: + addons_dir = get_local_dir("addons") + os.environ["AYON_ADDONS_DIR"] = addons_dir + return addons_dir + + +def get_dependencies_dir(): + """Directory where dependency packages are stored. + + Path to addons is defined using python module 'appdirs' which + + The path is stored into environment variable 'AYON_DEPENDENCIES_DIR'. + Value of environment variable can be overriden, but we highly recommended + to use that option only for development purposes. + + Returns: + str: Path to directory where dependency packages should be downloaded. + """ + + dependencies_dir = os.environ.get("AYON_DEPENDENCIES_DIR") + if not dependencies_dir: + dependencies_dir = get_local_dir("dependency_packages") + os.environ["AYON_DEPENDENCIES_DIR"] = dependencies_dir + return dependencies_dir + + +class SourceDownloader(metaclass=ABCMeta): + log = logging.getLogger(__name__) + + @classmethod + @abstractmethod + def download(cls, source, destination_dir, data, transfer_progress): + """Returns url to downloaded addon zip file. + + Tranfer progress can be ignored, in that case file transfer won't + be shown as 0-100% but as 'running'. First step should be to set + destination content size and then add transferred chunk sizes. + + Args: + source (dict): {type:"http", "url":"https://} ...} + destination_dir (str): local folder to unzip + data (dict): More information about download content. Always have + 'type' key in. + transfer_progress (ayon_api.TransferProgress): Progress of + transferred (copy/download) content. + + Returns: + (str) local path to addon zip file + """ + + pass + + @classmethod + @abstractmethod + def cleanup(cls, source, destination_dir, data): + """Cleanup files when distribution finishes or crashes. + + Cleanup e.g. temporary files (downloaded zip) or other related stuff + to downloader. + """ + + pass + + @classmethod + def check_hash(cls, addon_path, addon_hash, hash_type="sha256"): + """Compares 'hash' of downloaded 'addon_url' file. + + Args: + addon_path (str): Local path to addon file. + addon_hash (str): Hash of downloaded file. + hash_type (str): Type of hash. + + Raises: + ValueError if hashes doesn't match + """ + + if not os.path.exists(addon_path): + raise ValueError(f"{addon_path} doesn't exist.") + if not RemoteFileHandler.check_integrity(addon_path, + addon_hash, + hash_type=hash_type): + raise ValueError(f"{addon_path} doesn't match expected hash.") + + @classmethod + def unzip(cls, addon_zip_path, destination_dir): + """Unzips local 'addon_zip_path' to 'destination'. + + Args: + addon_zip_path (str): local path to addon zip file + destination_dir (str): local folder to unzip + """ + + RemoteFileHandler.unzip(addon_zip_path, destination_dir) + os.remove(addon_zip_path) + + +class DownloadFactory: + def __init__(self): + self._downloaders = {} + + def register_format(self, downloader_type, downloader): + """Register downloader for download type. + + Args: + downloader_type (UrlType): Type of source. + downloader (SourceDownloader): Downloader which cares about + download, hash check and unzipping. + """ + + self._downloaders[downloader_type.value] = downloader + + def get_downloader(self, downloader_type): + """Registered downloader for type. + + Args: + downloader_type (UrlType): Type of source. + + Returns: + SourceDownloader: Downloader object which should care about file + distribution. + + Raises: + ValueError: If type does not have registered downloader. + """ + + if downloader := self._downloaders.get(downloader_type): + return downloader() + raise ValueError(f"{downloader_type} not implemented") + + +class OSDownloader(SourceDownloader): + @classmethod + def download(cls, source, destination_dir, data, transfer_progress): + # OS doesn't need to download, unzip directly + addon_url = source["path"].get(platform.system().lower()) + if not os.path.exists(addon_url): + raise ValueError(f"{addon_url} is not accessible") + return addon_url + + @classmethod + def cleanup(cls, source, destination_dir, data): + # Nothing to do - download does not copy anything + pass + + +class HTTPDownloader(SourceDownloader): + CHUNK_SIZE = 100000 + + @staticmethod + def get_filename(source): + source_url = source["url"] + filename = source.get("filename") + if not filename: + filename = os.path.basename(source_url) + basename, ext = os.path.splitext(filename) + allowed_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS) + if ext.replace(".", "") not in allowed_exts: + filename = f"{basename}.zip" + return filename + + @classmethod + def download(cls, source, destination_dir, data, transfer_progress): + source_url = source["url"] + cls.log.debug(f"Downloading {source_url} to {destination_dir}") + headers = source.get("headers") + filename = cls.get_filename(source) + + # TODO use transfer progress + RemoteFileHandler.download_url( + source_url, + destination_dir, + filename, + headers=headers + ) + + return os.path.join(destination_dir, filename) + + @classmethod + def cleanup(cls, source, destination_dir, data): + # Nothing to do - download does not copy anything + filename = cls.get_filename(source) + filepath = os.path.join(destination_dir, filename) + if os.path.exists(filepath) and os.path.isfile(filepath): + os.remove(filepath) + + +class AyonServerDownloader(SourceDownloader): + """Downloads static resource file from v4 Server. + + Expects filled env var AYON_SERVER_URL. + """ + + CHUNK_SIZE = 8192 + + @classmethod + def download(cls, source, destination_dir, data, transfer_progress): + path = source["path"] + filename = source["filename"] + if path and not filename: + filename = path.split("/")[-1] + + cls.log.debug(f"Downloading {filename} to {destination_dir}") + + _, ext = os.path.splitext(filename) + clear_ext = ext.lower().replace(".", "") + valid_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS) + if clear_ext not in valid_exts: + raise ValueError( + "Invalid file extension \"{}\". Expected {}".format( + clear_ext, ", ".join(valid_exts) + )) + + if path: + filepath = os.path.join(destination_dir, filename) + return ayon_api.download_file( + path, + filepath, + chunk_size=cls.CHUNK_SIZE, + progress=transfer_progress + ) + + # dst_filepath = os.path.join(destination_dir, filename) + if data["type"] == "dependency_package": + return ayon_api.download_dependency_package( + data["name"], + destination_dir, + filename, + platform_name=data["platform"], + chunk_size=cls.CHUNK_SIZE, + progress=transfer_progress + ) + + if data["type"] == "addon": + return ayon_api.download_addon_private_file( + data["name"], + data["version"], + filename, + destination_dir, + chunk_size=cls.CHUNK_SIZE, + progress=transfer_progress + ) + + raise ValueError(f"Unknown type to download \"{data['type']}\"") + + @classmethod + def cleanup(cls, source, destination_dir, data): + # Nothing to do - download does not copy anything + filename = source["filename"] + filepath = os.path.join(destination_dir, filename) + if os.path.exists(filepath) and os.path.isfile(filepath): + os.remove(filepath) + + +def get_addons_info(): + """Returns list of addon information from Server + + Returns: + List[AddonInfo]: List of metadata for addons sent from server, + parsed in AddonInfo objects + """ + + addons_info = [] + for addon in ayon_api.get_addons_info(details=True)["addons"]: + addon_info = AddonInfo.from_dict(addon) + if addon_info is not None: + addons_info.append(addon_info) + return addons_info + + +def get_dependency_package(package_name=None): + """Returns info about currently used dependency package. + + Dependency package means .venv created from all activated addons from the + server (plus libraries for core Tray app TODO confirm). + This package needs to be downloaded, unpacked and added to sys.path for + Tray app to work. + + Args: + package_name (str): Name of package. Production package name is used + if not entered. + + Returns: + Union[DependencyItem, None]: Item or None if package with the name was + not found. + """ + + dependencies_info = ayon_api.get_dependencies_info() + + dependency_list = dependencies_info["packages"] + # Use production package if package is not specified + if package_name is None: + package_name = dependencies_info["productionPackage"] + + for dependency in dependency_list: + dependency_package = DependencyItem.from_dict(dependency) + if dependency_package.name == package_name: + return dependency_package + + +class DistributeTransferProgress: + """Progress of single source item in 'DistributionItem'. + + The item is to keep track of single source item. + """ + + def __init__(self): + self._transfer_progress = ayon_api.TransferProgress() + self._started = False + self._failed = False + self._fail_reason = None + self._unzip_started = False + self._unzip_finished = False + self._hash_check_started = False + self._hash_check_finished = False + + def set_started(self): + """Call when source distribution starts.""" + + self._started = True + + def set_failed(self, reason): + """Set source distribution as failed. + + Args: + reason (str): Error message why the transfer failed. + """ + + self._failed = True + self._fail_reason = reason + + def set_hash_check_started(self): + """Call just before hash check starts.""" + + self._hash_check_started = True + + def set_hash_check_finished(self): + """Call just after hash check finishes.""" + + self._hash_check_finished = True + + def set_unzip_started(self): + """Call just before unzip starts.""" + + self._unzip_started = True + + def set_unzip_finished(self): + """Call just after unzip finishes.""" + + self._unzip_finished = True + + @property + def is_running(self): + """Source distribution is in progress. + + Returns: + bool: Transfer is in progress. + """ + + return bool( + self._started + and not self._failed + and not self._hash_check_finished + ) + + @property + def transfer_progress(self): + """Source file 'download' progress tracker. + + Returns: + ayon_api.TransferProgress.: Content download progress. + """ + + return self._transfer_progress + + @property + def started(self): + return self._started + + @property + def hash_check_started(self): + return self._hash_check_started + + @property + def hash_check_finished(self): + return self._has_check_finished + + @property + def unzip_started(self): + return self._unzip_started + + @property + def unzip_finished(self): + return self._unzip_finished + + @property + def failed(self): + return self._failed or self._transfer_progress.failed + + @property + def fail_reason(self): + return self._fail_reason or self._transfer_progress.fail_reason + + +class DistributionItem: + """Distribution item with sources and target directories. + + Distribution item can be an addon or dependency package. Distribution item + can be already distributed and don't need any progression. The item keeps + track of the progress. The reason is to be able to use the distribution + items as source data for UI without implementing the same logic. + + Distribution is "state" based. Distribution can be 'UPDATED' or 'OUTDATED' + at the initialization. If item is 'UPDATED' the distribution is skipped + and 'OUTDATED' will trigger the distribution process. + + Because the distribution may have multiple sources each source has own + progress item. + + Args: + state (UpdateState): Initial state (UpdateState.UPDATED or + UpdateState.OUTDATED). + unzip_dirpath (str): Path to directory where zip is downloaded. + download_dirpath (str): Path to directory where file is unzipped. + file_hash (str): Hash of file for validation. + factory (DownloadFactory): Downloaders factory object. + sources (List[SourceInfo]): Possible sources to receive the + distribution item. + downloader_data (Dict[str, Any]): More information for downloaders. + item_label (str): Label used in log outputs (and in UI). + logger (logging.Logger): Logger object. + """ + + def __init__( + self, + state, + unzip_dirpath, + download_dirpath, + file_hash, + factory, + sources, + downloader_data, + item_label, + logger=None, + ): + if logger is None: + logger = logging.getLogger(self.__class__.__name__) + self.log = logger + self.state = state + self.unzip_dirpath = unzip_dirpath + self.download_dirpath = download_dirpath + self.file_hash = file_hash + self.factory = factory + self.sources = [ + (source, DistributeTransferProgress()) + for source in sources + ] + self.downloader_data = downloader_data + self.item_label = item_label + + self._need_distribution = state != UpdateState.UPDATED + self._current_source_progress = None + self._used_source_progress = None + self._used_source = None + self._dist_started = False + self._dist_finished = False + + self._error_msg = None + self._error_detail = None + + @property + def need_distribution(self): + """Need distribution based on initial state. + + Returns: + bool: Need distribution. + """ + + return self._need_distribution + + @property + def current_source_progress(self): + """Currently processed source progress object. + + Returns: + Union[DistributeTransferProgress, None]: Transfer progress or None. + """ + + return self._current_source_progress + + @property + def used_source_progress(self): + """Transfer progress that successfully distributed the item. + + Returns: + Union[DistributeTransferProgress, None]: Transfer progress or None. + """ + + return self._used_source_progress + + @property + def used_source(self): + """Data of source item. + + Returns: + Union[Dict[str, Any], None]: SourceInfo data or None. + """ + + return self._used_source + + @property + def error_message(self): + """Reason why distribution item failed. + + Returns: + Union[str, None]: Error message. + """ + + return self._error_msg + + @property + def error_detail(self): + """Detailed reason why distribution item failed. + + Returns: + Union[str, None]: Detailed information (maybe traceback). + """ + + return self._error_detail + + def _distribute(self): + if not self.sources: + message = ( + f"{self.item_label}: Don't have" + " any sources to download from." + ) + self.log.error(message) + self._error_msg = message + self.state = UpdateState.MISS_SOURCE_FILES + return + + download_dirpath = self.download_dirpath + unzip_dirpath = self.unzip_dirpath + for source, source_progress in self.sources: + self._current_source_progress = source_progress + source_progress.set_started() + + # Remove directory if exists + if os.path.isdir(unzip_dirpath): + self.log.debug(f"Cleaning {unzip_dirpath}") + shutil.rmtree(unzip_dirpath) + + # Create directory + os.makedirs(unzip_dirpath) + if not os.path.isdir(download_dirpath): + os.makedirs(download_dirpath) + + try: + downloader = self.factory.get_downloader(source.type) + except Exception: + source_progress.set_failed(f"Unknown downloader {source.type}") + self.log.warning(message, exc_info=True) + continue + + source_data = attr.asdict(source) + cleanup_args = ( + source_data, + download_dirpath, + self.downloader_data + ) + + try: + zip_filepath = downloader.download( + source_data, + download_dirpath, + self.downloader_data, + source_progress.transfer_progress, + ) + except Exception: + message = "Failed to download source" + source_progress.set_failed(message) + self.log.warning( + f"{self.item_label}: {message}", + exc_info=True + ) + downloader.cleanup(*cleanup_args) + continue + + source_progress.set_hash_check_started() + try: + downloader.check_hash(zip_filepath, self.file_hash) + except Exception: + message = "File hash does not match" + source_progress.set_failed(message) + self.log.warning( + f"{self.item_label}: {message}", + exc_info=True + ) + downloader.cleanup(*cleanup_args) + continue + + source_progress.set_hash_check_finished() + source_progress.set_unzip_started() + try: + downloader.unzip(zip_filepath, unzip_dirpath) + except Exception: + message = "Couldn't unzip source file" + source_progress.set_failed(message) + self.log.warning( + f"{self.item_label}: {message}", + exc_info=True + ) + downloader.cleanup(*cleanup_args) + continue + + source_progress.set_unzip_finished() + downloader.cleanup(*cleanup_args) + self.state = UpdateState.UPDATED + self._used_source = source_data + break + + last_progress = self._current_source_progress + self._current_source_progress = None + if self.state == UpdateState.UPDATED: + self._used_source_progress = last_progress + self.log.info(f"{self.item_label}: Distributed") + return + + self.log.error(f"{self.item_label}: Failed to distribute") + self._error_msg = "Failed to receive or install source files" + + def distribute(self): + """Execute distribution logic.""" + + if not self.need_distribution or self._dist_started: + return + + self._dist_started = True + try: + if self.state == UpdateState.OUTDATED: + self._distribute() + + except Exception as exc: + self.state = UpdateState.UPDATE_FAILED + self._error_msg = str(exc) + self._error_detail = "".join( + traceback.format_exception(*sys.exc_info()) + ) + self.log.error( + f"{self.item_label}: Distibution filed", + exc_info=True + ) + + finally: + self._dist_finished = True + if self.state == UpdateState.OUTDATED: + self.state = UpdateState.UPDATE_FAILED + self._error_msg = "Distribution failed" + + if ( + self.state != UpdateState.UPDATED + and self.unzip_dirpath + and os.path.isdir(self.unzip_dirpath) + ): + self.log.debug(f"Cleaning {self.unzip_dirpath}") + shutil.rmtree(self.unzip_dirpath) + + +class AyonDistribution: + """Distribution control. + + Receive information from server what addons and dependency packages + should be available locally and prepare/validate their distribution. + + Arguments are available for testing of the class. + + Args: + addon_dirpath (str): Where addons will be stored. + dependency_dirpath (str): Where dependencies will be stored. + dist_factory (DownloadFactory): Factory which cares about downloading + of items based on source type. + addons_info (List[AddonInfo]): List of prepared addons info. + dependency_package_info (Union[Dict[str, Any], None]): Dependency + package info from server. Defaults to '-1'. + """ + + def __init__( + self, + addon_dirpath=None, + dependency_dirpath=None, + dist_factory=None, + addons_info=None, + dependency_package_info=-1, + ): + self._addons_dirpath = addon_dirpath or get_addons_dir() + self._dependency_dirpath = dependency_dirpath or get_dependencies_dir() + self._dist_factory = ( + dist_factory or get_default_download_factory() + ) + + if isinstance(addons_info, list): + addons_info = {item.full_name: item for item in addons_info} + self._dist_started = False + self._dist_finished = False + self._log = None + self._addons_info = addons_info + self._addons_dist_items = None + self._dependency_package = dependency_package_info + self._dependency_dist_item = -1 + + @property + def log(self): + if self._log is None: + self._log = logging.getLogger(self.__class__.__name__) + return self._log + + @property + def addons_info(self): + """Information about available addons on server. + + Addons may require distribution of files. For those addons will be + created 'DistributionItem' handling distribution itself. + + Todos: + Add support for staging versions. Right now is supported only + production version. + + Returns: + Dict[str, AddonInfo]: Addon info by full name. + """ + + if self._addons_info is None: + addons_info = {} + server_addons_info = ayon_api.get_addons_info(details=True) + for addon in server_addons_info["addons"]: + addon_info = AddonInfo.from_dict(addon) + if addon_info is None: + continue + addons_info[addon_info.full_name] = addon_info + + self._addons_info = addons_info + return self._addons_info + + @property + def dependency_package(self): + """Information about dependency package from server. + + Receive and cache dependency package information from server. + + Notes: + For testing purposes it is possible to pass dependency package + information to '__init__'. + + Returns: + Union[None, Dict[str, Any]]: None if server does not have specified + dependency package. + """ + + if self._dependency_package == -1: + self._dependency_package = get_dependency_package() + return self._dependency_package + + def _prepare_current_addons_dist_items(self): + addons_metadata = self.get_addons_metadata() + output = {} + for full_name, addon_info in self.addons_info.items(): + if not addon_info.require_distribution: + continue + addon_dest = os.path.join(self._addons_dirpath, full_name) + self.log.debug(f"Checking {full_name} in {addon_dest}") + addon_in_metadata = ( + addon_info.name in addons_metadata + and addon_info.version in addons_metadata[addon_info.name] + ) + if addon_in_metadata and os.path.isdir(addon_dest): + self.log.debug( + f"Addon version folder {addon_dest} already exists." + ) + state = UpdateState.UPDATED + + else: + state = UpdateState.OUTDATED + + downloader_data = { + "type": "addon", + "name": addon_info.name, + "version": addon_info.version + } + + output[full_name] = DistributionItem( + state, + addon_dest, + addon_dest, + addon_info.hash, + self._dist_factory, + list(addon_info.sources), + downloader_data, + full_name, + self.log + ) + return output + + def _prepare_dependency_progress(self): + package = self.dependency_package + if package is None or not package.require_distribution: + return None + + metadata = self.get_dependency_metadata() + downloader_data = { + "type": "dependency_package", + "name": package.name, + "platform": package.platform + } + zip_dir = package_dir = os.path.join( + self._dependency_dirpath, package.name + ) + self.log.debug(f"Checking {package.name} in {package_dir}") + + if not os.path.isdir(package_dir) or package.name not in metadata: + state = UpdateState.OUTDATED + else: + state = UpdateState.UPDATED + + return DistributionItem( + state, + zip_dir, + package_dir, + package.checksum, + self._dist_factory, + package.sources, + downloader_data, + package.name, + self.log, + ) + + def get_addons_dist_items(self): + """Addon distribution items. + + These items describe source files required by addon to be available on + machine. Each item may have 0-n source information from where can be + obtained. If file is already available it's state will be 'UPDATED'. + + Returns: + Dict[str, DistributionItem]: Distribution items by addon fullname. + """ + + if self._addons_dist_items is None: + self._addons_dist_items = self._prepare_current_addons_dist_items() + return self._addons_dist_items + + def get_dependency_dist_item(self): + """Dependency package distribution item. + + Item describe source files required by server to be available on + machine. Item may have 0-n source information from where can be + obtained. If file is already available it's state will be 'UPDATED'. + + 'None' is returned if server does not have defined any dependency + package. + + Returns: + Union[None, DistributionItem]: Dependency item or None if server + does not have specified any dependency package. + """ + + if self._dependency_dist_item == -1: + self._dependency_dist_item = self._prepare_dependency_progress() + return self._dependency_dist_item + + def get_dependency_metadata_filepath(self): + """Path to distribution metadata file. + + Metadata contain information about distributed packages, used source, + expected file hash and time when file was distributed. + + Returns: + str: Path to a file where dependency package metadata are stored. + """ + + return os.path.join(self._dependency_dirpath, "dependency.json") + + def get_addons_metadata_filepath(self): + """Path to addons metadata file. + + Metadata contain information about distributed addons, used sources, + expected file hashes and time when files were distributed. + + Returns: + str: Path to a file where addons metadata are stored. + """ + + return os.path.join(self._addons_dirpath, "addons.json") + + def read_metadata_file(self, filepath, default_value=None): + """Read json file from path. + + Method creates the file when does not exist with default value. + + Args: + filepath (str): Path to json file. + default_value (Union[Dict[str, Any], List[Any], None]): Default + value if the file is not available (or valid). + + Returns: + Union[Dict[str, Any], List[Any]]: Value from file. + """ + + if default_value is None: + default_value = {} + + if not os.path.exists(filepath): + return default_value + + try: + with open(filepath, "r") as stream: + data = json.load(stream) + except ValueError: + data = default_value + return data + + def save_metadata_file(self, filepath, data): + """Store data to json file. + + Method creates the file when does not exist. + + Args: + filepath (str): Path to json file. + data (Union[Dict[str, Any], List[Any]]): Data to store into file. + """ + + if not os.path.exists(filepath): + dirpath = os.path.dirname(filepath) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + with open(filepath, "w") as stream: + json.dump(data, stream, indent=4) + + def get_dependency_metadata(self): + filepath = self.get_dependency_metadata_filepath() + return self.read_metadata_file(filepath, {}) + + def update_dependency_metadata(self, package_name, data): + dependency_metadata = self.get_dependency_metadata() + dependency_metadata[package_name] = data + filepath = self.get_dependency_metadata_filepath() + self.save_metadata_file(filepath, dependency_metadata) + + def get_addons_metadata(self): + filepath = self.get_addons_metadata_filepath() + return self.read_metadata_file(filepath, {}) + + def update_addons_metadata(self, addons_information): + if not addons_information: + return + addons_metadata = self.get_addons_metadata() + for addon_name, version_value in addons_information.items(): + if addon_name not in addons_metadata: + addons_metadata[addon_name] = {} + for addon_version, version_data in version_value.items(): + addons_metadata[addon_name][addon_version] = version_data + + filepath = self.get_addons_metadata_filepath() + self.save_metadata_file(filepath, addons_metadata) + + def finish_distribution(self): + """Store metadata about distributed items.""" + + self._dist_finished = True + stored_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + dependency_dist_item = self.get_dependency_dist_item() + if ( + dependency_dist_item is not None + and dependency_dist_item.need_distribution + and dependency_dist_item.state == UpdateState.UPDATED + ): + package = self.dependency_package + source = dependency_dist_item.used_source + if source is not None: + data = { + "source": source, + "file_hash": dependency_dist_item.file_hash, + "distributed_dt": stored_time + } + self.update_dependency_metadata(package.name, data) + + addons_info = {} + for full_name, dist_item in self.get_addons_dist_items().items(): + if ( + not dist_item.need_distribution + or dist_item.state != UpdateState.UPDATED + ): + continue + + source_data = dist_item.used_source + if not source_data: + continue + addon_info = self.addons_info[full_name] + if addon_info.name not in addons_info: + addons_info[addon_info.name] = {} + addons_info[addon_info.name][addon_info.version] = { + "source": source_data, + "file_hash": dist_item.file_hash, + "distributed_dt": stored_time + } + + self.update_addons_metadata(addons_info) + + def get_all_distribution_items(self): + """Distribution items required by server. + + Items contain dependency package item and all addons that are enabled + and have distribution requirements. + + Items can be already available on machine. + + Returns: + List[DistributionItem]: Distribution items required by server. + """ + + output = [] + dependency_dist_item = self.get_dependency_dist_item() + if dependency_dist_item is not None: + output.append(dependency_dist_item) + for dist_item in self.get_addons_dist_items().values(): + output.append(dist_item) + return output + + def distribute(self, threaded=False): + """Distribute all missing items. + + Method will try to distribute all items that are required by server. + + This method does not handle failed items. To validate the result call + 'validate_distribution' when this method finishes. + + Args: + threaded (bool): Distribute items in threads. + """ + + if self._dist_started: + raise RuntimeError("Distribution already started") + self._dist_started = True + threads = collections.deque() + for item in self.get_all_distribution_items(): + if threaded: + threads.append(threading.Thread(target=item.distribute)) + else: + item.distribute() + + while threads: + thread = threads.popleft() + if thread.is_alive(): + threads.append(thread) + else: + thread.join() + + self.finish_distribution() + + def validate_distribution(self): + """Check if all required distribution items are distributed. + + Raises: + RuntimeError: Any of items is not available. + """ + + invalid = [] + dependency_package = self.get_dependency_dist_item() + if ( + dependency_package is not None + and dependency_package.state != UpdateState.UPDATED + ): + invalid.append("Dependency package") + + for addon_name, dist_item in self.get_addons_dist_items().items(): + if dist_item.state != UpdateState.UPDATED: + invalid.append(addon_name) + + if not invalid: + return + + raise RuntimeError("Failed to distribute {}".format( + ", ".join([f'"{item}"' for item in invalid]) + )) + + def get_sys_paths(self): + """Get all paths to python packages that should be added to python. + + These paths lead to addon directories and python dependencies in + dependency package. + + Todos: + Add dependency package directory to output. ATM is not structure of + dependency package 100% defined. + + Returns: + List[str]: Paths that should be added to 'sys.path' and + 'PYTHONPATH'. + """ + + output = [] + for item in self.get_all_distribution_items(): + if item.state != UpdateState.UPDATED: + continue + unzip_dirpath = item.unzip_dirpath + if unzip_dirpath and os.path.exists(unzip_dirpath): + output.append(unzip_dirpath) + return output + + +def get_default_download_factory(): + download_factory = DownloadFactory() + download_factory.register_format(UrlType.FILESYSTEM, OSDownloader) + download_factory.register_format(UrlType.HTTP, HTTPDownloader) + download_factory.register_format(UrlType.SERVER, AyonServerDownloader) + return download_factory + + +def cli(*args): + raise NotImplementedError diff --git a/common/ayon_common/distribution/addon_info.py b/common/ayon_common/distribution/addon_info.py new file mode 100644 index 0000000000..6da6f11ead --- /dev/null +++ b/common/ayon_common/distribution/addon_info.py @@ -0,0 +1,177 @@ +import attr +from enum import Enum + + +class UrlType(Enum): + HTTP = "http" + GIT = "git" + FILESYSTEM = "filesystem" + SERVER = "server" + + +@attr.s +class MultiPlatformPath(object): + windows = attr.ib(default=None) + linux = attr.ib(default=None) + darwin = attr.ib(default=None) + + +@attr.s +class SourceInfo(object): + type = attr.ib() + + +@attr.s +class LocalSourceInfo(SourceInfo): + path = attr.ib(default=attr.Factory(MultiPlatformPath)) + + +@attr.s +class WebSourceInfo(SourceInfo): + url = attr.ib(default=None) + headers = attr.ib(default=None) + filename = attr.ib(default=None) + + +@attr.s +class ServerSourceInfo(SourceInfo): + filename = attr.ib(default=None) + path = attr.ib(default=None) + + +def convert_source(source): + """Create source object from data information. + + Args: + source (Dict[str, any]): Information about source. + + Returns: + Union[None, SourceInfo]: Object with source information if type is + known. + """ + + source_type = source.get("type") + if not source_type: + return None + + if source_type == UrlType.FILESYSTEM.value: + return LocalSourceInfo( + type=source_type, + path=source["path"] + ) + + if source_type == UrlType.HTTP.value: + url = source["path"] + return WebSourceInfo( + type=source_type, + url=url, + headers=source.get("headers"), + filename=source.get("filename") + ) + + if source_type == UrlType.SERVER.value: + return ServerSourceInfo( + type=source_type, + filename=source.get("filename"), + path=source.get("path") + ) + + +@attr.s +class VersionData(object): + version_data = attr.ib(default=None) + + +@attr.s +class AddonInfo(object): + """Object matching json payload from Server""" + name = attr.ib() + version = attr.ib() + full_name = attr.ib() + title = attr.ib(default=None) + require_distribution = attr.ib(default=False) + sources = attr.ib(default=attr.Factory(list)) + unknown_sources = attr.ib(default=attr.Factory(list)) + hash = attr.ib(default=None) + description = attr.ib(default=None) + license = attr.ib(default=None) + authors = attr.ib(default=None) + + @classmethod + def from_dict(cls, data): + sources = [] + unknown_sources = [] + + production_version = data.get("productionVersion") + if not production_version: + return None + + # server payload contains info about all versions + # active addon must have 'productionVersion' and matching version info + version_data = data.get("versions", {})[production_version] + source_info = version_data.get("clientSourceInfo") + require_distribution = source_info is not None + for source in (source_info or []): + addon_source = convert_source(source) + if addon_source is not None: + sources.append(addon_source) + else: + unknown_sources.append(source) + print(f"Unknown source {source.get('type')}") + + full_name = "{}_{}".format(data["name"], production_version) + return cls( + name=data.get("name"), + version=production_version, + full_name=full_name, + require_distribution=require_distribution, + sources=sources, + unknown_sources=unknown_sources, + hash=data.get("hash"), + description=data.get("description"), + title=data.get("title"), + license=data.get("license"), + authors=data.get("authors") + ) + + +@attr.s +class DependencyItem(object): + """Object matching payload from Server about single dependency package""" + name = attr.ib() + platform = attr.ib() + checksum = attr.ib() + require_distribution = attr.ib() + sources = attr.ib(default=attr.Factory(list)) + unknown_sources = attr.ib(default=attr.Factory(list)) + addon_list = attr.ib(default=attr.Factory(list)) + python_modules = attr.ib(default=attr.Factory(dict)) + + @classmethod + def from_dict(cls, package): + sources = [] + unknown_sources = [] + package_sources = package.get("sources") + require_distribution = package_sources is not None + for source in (package_sources or []): + dependency_source = convert_source(source) + if dependency_source is not None: + sources.append(dependency_source) + else: + print(f"Unknown source {source.get('type')}") + unknown_sources.append(source) + + addon_list = [f"{name}_{version}" + for name, version in + package.get("supportedAddons").items()] + + return cls( + name=package.get("name"), + platform=package.get("platform"), + require_distribution=require_distribution, + sources=sources, + unknown_sources=unknown_sources, + checksum=package.get("checksum"), + addon_list=addon_list, + python_modules=package.get("pythonModules") + ) diff --git a/common/openpype_common/distribution/file_handler.py b/common/ayon_common/distribution/file_handler.py similarity index 86% rename from common/openpype_common/distribution/file_handler.py rename to common/ayon_common/distribution/file_handler.py index e649f143e9..a666b014f0 100644 --- a/common/openpype_common/distribution/file_handler.py +++ b/common/ayon_common/distribution/file_handler.py @@ -62,7 +62,7 @@ class RemoteFileHandler: return True if not hash_type: raise ValueError("Provide hash type, md5 or sha256") - if hash_type == 'md5': + if hash_type == "md5": return RemoteFileHandler.check_md5(fpath, hash_value) if hash_type == "sha256": return RemoteFileHandler.check_sha256(fpath, hash_value) @@ -70,7 +70,7 @@ class RemoteFileHandler: @staticmethod def download_url( url, root, filename=None, - sha256=None, max_redirect_hops=3 + sha256=None, max_redirect_hops=3, headers=None ): """Download a file from a url and place it in root. Args: @@ -82,6 +82,7 @@ class RemoteFileHandler: If None, do not check max_redirect_hops (int, optional): Maximum number of redirect hops allowed + headers (dict): additional required headers - Authentication etc.. """ root = os.path.expanduser(root) if not filename: @@ -93,12 +94,13 @@ class RemoteFileHandler: # check if file is already present locally if RemoteFileHandler.check_integrity(fpath, sha256, hash_type="sha256"): - print('Using downloaded and verified file: ' + fpath) + print(f"Using downloaded and verified file: {fpath}") return # expand redirect chain if needed url = RemoteFileHandler._get_redirect_url(url, - max_hops=max_redirect_hops) + max_hops=max_redirect_hops, + headers=headers) # check if file is located on Google Drive file_id = RemoteFileHandler._get_google_drive_file_id(url) @@ -108,14 +110,17 @@ class RemoteFileHandler: # download the file try: - print('Downloading ' + url + ' to ' + fpath) - RemoteFileHandler._urlretrieve(url, fpath) + print(f"Downloading {url} to {fpath}") + RemoteFileHandler._urlretrieve(url, fpath, headers=headers) except (urllib.error.URLError, IOError) as e: - if url[:5] == 'https': - url = url.replace('https:', 'http:') - print('Failed download. Trying https -> http instead.' - ' Downloading ' + url + ' to ' + fpath) - RemoteFileHandler._urlretrieve(url, fpath) + if url[:5] == "https": + url = url.replace("https:", "http:") + print(( + "Failed download. Trying https -> http instead." + f" Downloading {url} to {fpath}" + )) + RemoteFileHandler._urlretrieve(url, fpath, + headers=headers) else: raise e @@ -216,11 +221,16 @@ class RemoteFileHandler: tar_file.close() @staticmethod - def _urlretrieve(url, filename, chunk_size): + def _urlretrieve(url, filename, chunk_size=None, headers=None): + final_headers = {"User-Agent": USER_AGENT} + if headers: + final_headers.update(headers) + + chunk_size = chunk_size or 8192 with open(filename, "wb") as fh: with urllib.request.urlopen( urllib.request.Request(url, - headers={"User-Agent": USER_AGENT})) \ + headers=final_headers)) \ as response: for chunk in iter(lambda: response.read(chunk_size), ""): if not chunk: @@ -228,13 +238,15 @@ class RemoteFileHandler: fh.write(chunk) @staticmethod - def _get_redirect_url(url, max_hops): + def _get_redirect_url(url, max_hops, headers=None): initial_url = url - headers = {"Method": "HEAD", "User-Agent": USER_AGENT} - + final_headers = {"Method": "HEAD", "User-Agent": USER_AGENT} + if headers: + final_headers.update(headers) for _ in range(max_hops + 1): with urllib.request.urlopen( - urllib.request.Request(url, headers=headers)) as response: + urllib.request.Request(url, + headers=final_headers)) as response: if response.url == url or response.url is None: return url diff --git a/common/openpype_common/distribution/tests/test_addon_distributtion.py b/common/ayon_common/distribution/tests/test_addon_distributtion.py similarity index 63% rename from common/openpype_common/distribution/tests/test_addon_distributtion.py rename to common/ayon_common/distribution/tests/test_addon_distributtion.py index 765ea0596a..22a347f3eb 100644 --- a/common/openpype_common/distribution/tests/test_addon_distributtion.py +++ b/common/ayon_common/distribution/tests/test_addon_distributtion.py @@ -2,29 +2,29 @@ import pytest import attr import tempfile -from common.openpype_common.distribution.addon_distribution import ( - AddonDownloader, - OSAddonDownloader, - HTTPAddonDownloader, +from common.ayon_common.distribution.addon_distribution import ( + DownloadFactory, + OSDownloader, + HTTPDownloader, AddonInfo, - update_addon_state, + AyonDistribution, UpdateState ) -from common.openpype_common.distribution.addon_info import UrlType +from common.ayon_common.distribution.addon_info import UrlType @pytest.fixture -def addon_downloader(): - addon_downloader = AddonDownloader() - addon_downloader.register_format(UrlType.FILESYSTEM, OSAddonDownloader) - addon_downloader.register_format(UrlType.HTTP, HTTPAddonDownloader) +def addon_download_factory(): + addon_downloader = DownloadFactory() + addon_downloader.register_format(UrlType.FILESYSTEM, OSDownloader) + addon_downloader.register_format(UrlType.HTTP, HTTPDownloader) yield addon_downloader @pytest.fixture -def http_downloader(addon_downloader): - yield addon_downloader.get_downloader(UrlType.HTTP.value) +def http_downloader(addon_download_factory): + yield addon_download_factory.get_downloader(UrlType.HTTP.value) @pytest.fixture @@ -55,7 +55,8 @@ def sample_addon_info(): "clientSourceInfo": [ { "type": "http", - "url": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing" # noqa + "path": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing", # noqa + "filename": "dummy.zip" }, { "type": "filesystem", @@ -84,19 +85,19 @@ def sample_addon_info(): def test_register(printer): - addon_downloader = AddonDownloader() + download_factory = DownloadFactory() - assert len(addon_downloader._downloaders) == 0, "Contains registered" + assert len(download_factory._downloaders) == 0, "Contains registered" - addon_downloader.register_format(UrlType.FILESYSTEM, OSAddonDownloader) - assert len(addon_downloader._downloaders) == 1, "Should contain one" + download_factory.register_format(UrlType.FILESYSTEM, OSDownloader) + assert len(download_factory._downloaders) == 1, "Should contain one" -def test_get_downloader(printer, addon_downloader): - assert addon_downloader.get_downloader(UrlType.FILESYSTEM.value), "Should find" # noqa +def test_get_downloader(printer, download_factory): + assert download_factory.get_downloader(UrlType.FILESYSTEM.value), "Should find" # noqa with pytest.raises(ValueError): - addon_downloader.get_downloader("unknown"), "Shouldn't find" + download_factory.get_downloader("unknown"), "Shouldn't find" def test_addon_info(printer, sample_addon_info): @@ -147,21 +148,36 @@ def test_addon_info(printer, sample_addon_info): def test_update_addon_state(printer, sample_addon_info, - temp_folder, addon_downloader): + temp_folder, download_factory): """Tests possible cases of addon update.""" addon_info = AddonInfo.from_dict(sample_addon_info) orig_hash = addon_info.hash + # Cause crash because of invalid hash addon_info.hash = "brokenhash" - result = update_addon_state([addon_info], temp_folder, addon_downloader) - assert result["openpype_slack_1.0.0"] == UpdateState.FAILED.value, \ - "Update should failed because of wrong hash" + distribution = AyonDistribution( + temp_folder, temp_folder, download_factory, [addon_info], None + ) + distribution.distribute() + dist_items = distribution.get_addons_dist_items() + slack_state = dist_items["openpype_slack_1.0.0"].state + assert slack_state == UpdateState.UPDATE_FAILED, ( + "Update should have failed because of wrong hash") + # Fix cache and validate if was updated addon_info.hash = orig_hash - result = update_addon_state([addon_info], temp_folder, addon_downloader) - assert result["openpype_slack_1.0.0"] == UpdateState.UPDATED.value, \ - "Addon should have been updated" + distribution = AyonDistribution( + temp_folder, temp_folder, download_factory, [addon_info], None + ) + distribution.distribute() + dist_items = distribution.get_addons_dist_items() + assert dist_items["openpype_slack_1.0.0"].state == UpdateState.UPDATED, ( + "Addon should have been updated") - result = update_addon_state([addon_info], temp_folder, addon_downloader) - assert result["openpype_slack_1.0.0"] == UpdateState.EXISTS.value, \ - "Addon should already exist" + # Is UPDATED without calling distribute + distribution = AyonDistribution( + temp_folder, temp_folder, download_factory, [addon_info], None + ) + dist_items = distribution.get_addons_dist_items() + assert dist_items["openpype_slack_1.0.0"].state == UpdateState.UPDATED, ( + "Addon should already exist") diff --git a/common/ayon_common/resources/AYON.icns b/common/ayon_common/resources/AYON.icns new file mode 100644 index 0000000000000000000000000000000000000000..2ec66cf3e0bb522c40209b5f1767ffd01014b0a2 GIT binary patch literal 40634 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

H};dB$t%vn!rC`t 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 literal 0 HcmV?d00001 diff --git a/common/ayon_common/resources/eye.png b/common/ayon_common/resources/eye.png new file mode 100644 index 0000000000000000000000000000000000000000..5a683e29748b35b3b64a200cb9723e82df45f4b9 GIT binary patch literal 2152 zcmai0dstIt93Gi@K}1AK5$ilhQ4HAG#Z7kHAlarv9U_+)52cRn3=U&EhwZ>N#7GK* zAtv}_f~X1RrMQNGK3E`_G;gFyW#kPtEFnU861>%SHUdqpKh8P(zW4pT=l6Sm-}jy6 zgoQ43ndmu@!C<(A$Ry$5Ii0@7zXa}AO`<^t_f`G3=Ox+lsoQqojl#IUJPsC?Dy)e-<~A9e{EIA z2qq1LIz@%4?PUQu2WliVlu2p87RQ4Ii{Ql?4G!$IKw#_O@p{Yvv6%v`A@WrELObEHO$y>1b71p>Qv?|~M!;a?Aj0(E^f7>A#^Nq7WiXsF zanP8j8p2@s=#T9iz0>8ZP<}T^%toqu=t8RV^cK;{W+B5aNrsb!i zj#T(0_uXz<(-bA}mF~T3cAV1g z`Q!@km7S*cAZ64)V9FAjd+^XsnOp`qd~uScqb=LTmL7JgneuAyqOcKHsI zId_6EJKHtG1L8K2p7W|o#06JKkN4dAK2^PY`sYoHems9H*`u;L2;Qe1qt=XRWoK4c z`=&9kMh#ub+j{HsTW6HhTip6)ZL8lR4v5`)as0-YCoL1&PUoDzxHdO^#@V&rp%2R- zuB+yu0Kf=!mq?PT=Hn*Dh_bC}42 zBhD+UzbsjhRc@WzTWj`@CfA(1(RHi*$1^4K;J~`G$2^Z7-)Oxf_?o*@>!JCD(-;^r z`2OYW?Rz-3!XuvbRSo+JR^__%tSZa@t-tu#u55D+bRgEl*?s+Lc0h);Az-T8?$|6{ zJ+Arl8p=;4AFQq(TOmE3{!#a#ika5AuQkmri;m>QzT?^#0DqM#>kQg9C5Ul7FTy@qaAA}HTsH7rzZRX#RRxAbp literal 0 HcmV?d00001 diff --git a/common/ayon_common/resources/stylesheet.css b/common/ayon_common/resources/stylesheet.css new file mode 100644 index 0000000000..732f44f6d1 --- /dev/null +++ b/common/ayon_common/resources/stylesheet.css @@ -0,0 +1,84 @@ +* { + font-size: 10pt; + font-family: "Noto Sans"; + font-weight: 450; + outline: none; +} + +QWidget { + color: #D3D8DE; + background: #2C313A; + border-radius: 0px; +} + +QWidget:disabled { + color: #5b6779; +} + +QLabel { + background: transparent; +} + +QPushButton { + text-align:center center; + border: 0px solid transparent; + border-radius: 0.2em; + padding: 3px 5px 3px 5px; + background: #434a56; +} + +QPushButton:hover { + background: rgba(168, 175, 189, 0.3); + color: #F0F2F5; +} + +QPushButton:pressed {} + +QPushButton:disabled { + background: #434a56; +} + +QLineEdit { + border: 1px solid #373D48; + border-radius: 0.3em; + background: #21252B; + padding: 0.1em; +} + +QLineEdit:disabled { + background: #2C313A; +} +QLineEdit:hover { + border-color: rgba(168, 175, 189, .3); +} +QLineEdit:focus { + border-color: rgb(92, 173, 214); +} + +QLineEdit[state="invalid"] { + border-color: #AA5050; +} + +#Separator { + background: rgba(75, 83, 98, 127); +} + +#PasswordBtn { + border: none; + padding: 0.1em; + background: transparent; +} + +#PasswordBtn:hover { + background: #434a56; +} + +#LikeDisabledInput { + background: #2C313A; +} +#LikeDisabledInput:hover { + border-color: #373D48; +} +#LikeDisabledInput:focus { + border-color: #373D48; +} \ No newline at end of file diff --git a/common/ayon_common/utils.py b/common/ayon_common/utils.py new file mode 100644 index 0000000000..bbf7f01607 --- /dev/null +++ b/common/ayon_common/utils.py @@ -0,0 +1,52 @@ +import os +import appdirs + + +def get_ayon_appdirs(*args): + """Local app data directory of AYON client. + + Args: + *args (Iterable[str]): Subdirectories/files in local app data dir. + + Returns: + str: Path to directory/file in local app data dir. + """ + + return os.path.join( + appdirs.user_data_dir("ayon", "ynput"), + *args + ) + + +def _create_local_site_id(): + """Create a local site identifier.""" + from coolname import generate_slug + + new_id = generate_slug(3) + + print("Created local site id \"{}\"".format(new_id)) + + return new_id + + +def get_local_site_id(): + """Get local site identifier. + + Site id is created if does not exist yet. + """ + + # used for background syncing + site_id = os.environ.get("AYON_SITE_ID") + if site_id: + return site_id + + site_id_path = get_ayon_appdirs("site_id") + if os.path.exists(site_id_path): + with open(site_id_path, "r") as stream: + site_id = stream.read() + + if not site_id: + site_id = _create_local_site_id() + with open(site_id_path, "w") as stream: + stream.write(site_id) + return site_id diff --git a/common/openpype_common/distribution/addon_distribution.py b/common/openpype_common/distribution/addon_distribution.py deleted file mode 100644 index 5e48639dec..0000000000 --- a/common/openpype_common/distribution/addon_distribution.py +++ /dev/null @@ -1,208 +0,0 @@ -import os -from enum import Enum -from abc import abstractmethod -import attr -import logging -import requests -import platform -import shutil - -from .file_handler import RemoteFileHandler -from .addon_info import AddonInfo - - -class UpdateState(Enum): - EXISTS = "exists" - UPDATED = "updated" - FAILED = "failed" - - -class AddonDownloader: - log = logging.getLogger(__name__) - - def __init__(self): - self._downloaders = {} - - def register_format(self, downloader_type, downloader): - self._downloaders[downloader_type.value] = downloader - - def get_downloader(self, downloader_type): - downloader = self._downloaders.get(downloader_type) - if not downloader: - raise ValueError(f"{downloader_type} not implemented") - return downloader() - - @classmethod - @abstractmethod - def download(cls, source, destination): - """Returns url to downloaded addon zip file. - - Args: - source (dict): {type:"http", "url":"https://} ...} - destination (str): local folder to unzip - Returns: - (str) local path to addon zip file - """ - pass - - @classmethod - def check_hash(cls, addon_path, addon_hash): - """Compares 'hash' of downloaded 'addon_url' file. - - Args: - addon_path (str): local path to addon zip file - addon_hash (str): sha256 hash of zip file - Raises: - ValueError if hashes doesn't match - """ - if not os.path.exists(addon_path): - raise ValueError(f"{addon_path} doesn't exist.") - if not RemoteFileHandler.check_integrity(addon_path, - addon_hash, - hash_type="sha256"): - raise ValueError(f"{addon_path} doesn't match expected hash.") - - @classmethod - def unzip(cls, addon_zip_path, destination): - """Unzips local 'addon_zip_path' to 'destination'. - - Args: - addon_zip_path (str): local path to addon zip file - destination (str): local folder to unzip - """ - RemoteFileHandler.unzip(addon_zip_path, destination) - os.remove(addon_zip_path) - - @classmethod - def remove(cls, addon_url): - pass - - -class OSAddonDownloader(AddonDownloader): - - @classmethod - def download(cls, source, destination): - # OS doesnt need to download, unzip directly - addon_url = source["path"].get(platform.system().lower()) - if not os.path.exists(addon_url): - raise ValueError("{} is not accessible".format(addon_url)) - return addon_url - - -class HTTPAddonDownloader(AddonDownloader): - CHUNK_SIZE = 100000 - - @classmethod - def download(cls, source, destination): - source_url = source["url"] - cls.log.debug(f"Downloading {source_url} to {destination}") - file_name = os.path.basename(destination) - _, ext = os.path.splitext(file_name) - if (ext.replace(".", '') not - in set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS)): - file_name += ".zip" - RemoteFileHandler.download_url(source_url, - destination, - filename=file_name) - - return os.path.join(destination, file_name) - - -def get_addons_info(server_endpoint): - """Returns list of addon information from Server""" - # TODO temp - # addon_info = AddonInfo( - # **{"name": "openpype_slack", - # "version": "1.0.0", - # "addon_url": "c:/projects/openpype_slack_1.0.0.zip", - # "type": UrlType.FILESYSTEM, - # "hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658"}) # noqa - # - # http_addon = AddonInfo( - # **{"name": "openpype_slack", - # "version": "1.0.0", - # "addon_url": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing", # noqa - # "type": UrlType.HTTP, - # "hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658"}) # noqa - - response = requests.get(server_endpoint) - if not response.ok: - raise Exception(response.text) - - addons_info = [] - for addon in response.json(): - addons_info.append(AddonInfo(**addon)) - return addons_info - - -def update_addon_state(addon_infos, destination_folder, factory, - log=None): - """Loops through all 'addon_infos', compares local version, unzips. - - Loops through server provided list of dictionaries with information about - available addons. Looks if each addon is already present and deployed. - If isn't, addon zip gets downloaded and unzipped into 'destination_folder'. - Args: - addon_infos (list of AddonInfo) - destination_folder (str): local path - factory (AddonDownloader): factory to get appropriate downloader per - addon type - log (logging.Logger) - Returns: - (dict): {"addon_full_name": UpdateState.value - (eg. "exists"|"updated"|"failed") - """ - if not log: - log = logging.getLogger(__name__) - - download_states = {} - for addon in addon_infos: - full_name = "{}_{}".format(addon.name, addon.version) - addon_dest = os.path.join(destination_folder, full_name) - - if os.path.isdir(addon_dest): - log.debug(f"Addon version folder {addon_dest} already exists.") - download_states[full_name] = UpdateState.EXISTS.value - continue - - for source in addon.sources: - download_states[full_name] = UpdateState.FAILED.value - try: - downloader = factory.get_downloader(source.type) - zip_file_path = downloader.download(attr.asdict(source), - addon_dest) - downloader.check_hash(zip_file_path, addon.hash) - downloader.unzip(zip_file_path, addon_dest) - download_states[full_name] = UpdateState.UPDATED.value - break - except Exception: - log.warning(f"Error happened during updating {addon.name}", - exc_info=True) - if os.path.isdir(addon_dest): - log.debug(f"Cleaning {addon_dest}") - shutil.rmtree(addon_dest) - - return download_states - - -def check_addons(server_endpoint, addon_folder, downloaders): - """Main entry point to compare existing addons with those on server. - - Args: - server_endpoint (str): url to v4 server endpoint - addon_folder (str): local dir path for addons - downloaders (AddonDownloader): factory of downloaders - - Raises: - (RuntimeError) if any addon failed update - """ - addons_info = get_addons_info(server_endpoint) - result = update_addon_state(addons_info, - addon_folder, - downloaders) - if UpdateState.FAILED.value in result.values(): - raise RuntimeError(f"Unable to update some addons {result}") - - -def cli(*args): - raise NotImplementedError diff --git a/common/openpype_common/distribution/addon_info.py b/common/openpype_common/distribution/addon_info.py deleted file mode 100644 index 00ece11f3b..0000000000 --- a/common/openpype_common/distribution/addon_info.py +++ /dev/null @@ -1,80 +0,0 @@ -import attr -from enum import Enum - - -class UrlType(Enum): - HTTP = "http" - GIT = "git" - FILESYSTEM = "filesystem" - - -@attr.s -class MultiPlatformPath(object): - windows = attr.ib(default=None) - linux = attr.ib(default=None) - darwin = attr.ib(default=None) - - -@attr.s -class AddonSource(object): - type = attr.ib() - - -@attr.s -class LocalAddonSource(AddonSource): - path = attr.ib(default=attr.Factory(MultiPlatformPath)) - - -@attr.s -class WebAddonSource(AddonSource): - url = attr.ib(default=None) - - -@attr.s -class VersionData(object): - version_data = attr.ib(default=None) - - -@attr.s -class AddonInfo(object): - """Object matching json payload from Server""" - name = attr.ib() - version = attr.ib() - title = attr.ib(default=None) - sources = attr.ib(default=attr.Factory(dict)) - hash = attr.ib(default=None) - description = attr.ib(default=None) - license = attr.ib(default=None) - authors = attr.ib(default=None) - - @classmethod - def from_dict(cls, data): - sources = [] - - production_version = data.get("productionVersion") - if not production_version: - return - - # server payload contains info about all versions - # active addon must have 'productionVersion' and matching version info - version_data = data.get("versions", {})[production_version] - - for source in version_data.get("clientSourceInfo", []): - if source.get("type") == UrlType.FILESYSTEM.value: - source_addon = LocalAddonSource(type=source["type"], - path=source["path"]) - if source.get("type") == UrlType.HTTP.value: - source_addon = WebAddonSource(type=source["type"], - url=source["url"]) - - sources.append(source_addon) - - return cls(name=data.get("name"), - version=production_version, - sources=sources, - hash=data.get("hash"), - description=data.get("description"), - title=data.get("title"), - license=data.get("license"), - authors=data.get("authors")) - diff --git a/openpype/__init__.py b/openpype/__init__.py index 810664707a..e6b77b1853 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -3,3 +3,5 @@ import os PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__)) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +AYON_SERVER_ENABLED = os.environ.get("USE_AYON_SERVER") == "1" diff --git a/openpype/client/entities.py b/openpype/client/entities.py index adbdd7a47c..5d9654c611 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1,1553 +1,6 @@ -"""Unclear if these will have public functions like these. +from openpype import AYON_SERVER_ENABLED -Goal is that most of functions here are called on (or with) an object -that has project name as a context (e.g. on 'ProjectEntity'?). - -+ We will need more specific functions doing very specific queries really fast. -""" - -import re -import collections - -import six -from bson.objectid import ObjectId - -from .mongo import get_project_database, get_project_connection - -PatternType = type(re.compile("")) - - -def _prepare_fields(fields, required_fields=None): - if not fields: - return None - - output = { - field: True - for field in fields - } - if "_id" not in output: - output["_id"] = True - - if required_fields: - for key in required_fields: - output[key] = True - return output - - -def convert_id(in_id): - """Helper function for conversion of id from string to ObjectId. - - Args: - in_id (Union[str, ObjectId, Any]): Entity id that should be converted - to right type for queries. - - Returns: - Union[ObjectId, Any]: Converted ids to ObjectId or in type. - """ - - if isinstance(in_id, six.string_types): - return ObjectId(in_id) - return in_id - - -def convert_ids(in_ids): - """Helper function for conversion of ids from string to ObjectId. - - Args: - in_ids (Iterable[Union[str, ObjectId, Any]]): List of entity ids that - should be converted to right type for queries. - - Returns: - List[ObjectId]: Converted ids to ObjectId. - """ - - _output = set() - for in_id in in_ids: - if in_id is not None: - _output.add(convert_id(in_id)) - return list(_output) - - -def get_projects(active=True, inactive=False, fields=None): - """Yield all project entity documents. - - Args: - active (Optional[bool]): Include active projects. Defaults to True. - inactive (Optional[bool]): Include inactive projects. - Defaults to False. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Yields: - dict: Project entity data which can be reduced to specified 'fields'. - None is returned if project with specified filters was not found. - """ - mongodb = get_project_database() - for project_name in mongodb.collection_names(): - if project_name in ("system.indexes",): - continue - project_doc = get_project( - project_name, active=active, inactive=inactive, fields=fields - ) - if project_doc is not None: - yield project_doc - - -def get_project(project_name, active=True, inactive=True, fields=None): - """Return project entity document by project name. - - Args: - project_name (str): Name of project. - active (Optional[bool]): Allow active project. Defaults to True. - inactive (Optional[bool]): Allow inactive project. Defaults to True. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Project entity data which can be reduced to - specified 'fields'. None is returned if project with specified - filters was not found. - """ - # Skip if both are disabled - if not active and not inactive: - return None - - query_filter = {"type": "project"} - # Keep query untouched if both should be available - if active and inactive: - pass - - # Add filter to keep only active - elif active: - query_filter["$or"] = [ - {"data.active": {"$exists": False}}, - {"data.active": True}, - ] - - # Add filter to keep only inactive - elif inactive: - query_filter["$or"] = [ - {"data.active": {"$exists": False}}, - {"data.active": False}, - ] - - conn = get_project_connection(project_name) - return conn.find_one(query_filter, _prepare_fields(fields)) - - -def get_whole_project(project_name): - """Receive all documents from project. - - Helper that can be used to get all document from whole project. For example - for backups etc. - - Returns: - Cursor: Query cursor as iterable which returns all documents from - project collection. - """ - - conn = get_project_connection(project_name) - return conn.find({}) - - -def get_asset_by_id(project_name, asset_id, fields=None): - """Receive asset data by its id. - - Args: - project_name (str): Name of project where to look for queried entities. - asset_id (Union[str, ObjectId]): Asset's id. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Asset entity data which can be reduced to - specified 'fields'. None is returned if asset with specified - filters was not found. - """ - - asset_id = convert_id(asset_id) - if not asset_id: - return None - - query_filter = {"type": "asset", "_id": asset_id} - conn = get_project_connection(project_name) - return conn.find_one(query_filter, _prepare_fields(fields)) - - -def get_asset_by_name(project_name, asset_name, fields=None): - """Receive asset data by its name. - - Args: - project_name (str): Name of project where to look for queried entities. - asset_name (str): Asset's name. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Asset entity data which can be reduced to - specified 'fields'. None is returned if asset with specified - filters was not found. - """ - - if not asset_name: - return None - - query_filter = {"type": "asset", "name": asset_name} - conn = get_project_connection(project_name) - return conn.find_one(query_filter, _prepare_fields(fields)) - - -# NOTE this could be just public function? -# - any better variable name instead of 'standard'? -# - same approach can be used for rest of types -def _get_assets( - project_name, - asset_ids=None, - asset_names=None, - parent_ids=None, - standard=True, - archived=False, - fields=None -): - """Assets for specified project by passed filters. - - Passed filters (ids and names) are always combined so all conditions must - match. - - To receive all assets from project just keep filters empty. - - Args: - project_name (str): Name of project where to look for queried entities. - asset_ids (Iterable[Union[str, ObjectId]]): Asset ids that should - be found. - asset_names (Iterable[str]): Name assets that should be found. - parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. - standard (bool): Query standard assets (type 'asset'). - archived (bool): Query archived assets (type 'archived_asset'). - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Cursor: Query cursor as iterable which returns asset documents matching - passed filters. - """ - - asset_types = [] - if standard: - asset_types.append("asset") - if archived: - asset_types.append("archived_asset") - - if not asset_types: - return [] - - if len(asset_types) == 1: - query_filter = {"type": asset_types[0]} - else: - query_filter = {"type": {"$in": asset_types}} - - if asset_ids is not None: - asset_ids = convert_ids(asset_ids) - if not asset_ids: - return [] - query_filter["_id"] = {"$in": asset_ids} - - if asset_names is not None: - if not asset_names: - return [] - query_filter["name"] = {"$in": list(asset_names)} - - if parent_ids is not None: - parent_ids = convert_ids(parent_ids) - if not parent_ids: - return [] - query_filter["data.visualParent"] = {"$in": parent_ids} - - conn = get_project_connection(project_name) - - return conn.find(query_filter, _prepare_fields(fields)) - - -def get_assets( - project_name, - asset_ids=None, - asset_names=None, - parent_ids=None, - archived=False, - fields=None -): - """Assets for specified project by passed filters. - - Passed filters (ids and names) are always combined so all conditions must - match. - - To receive all assets from project just keep filters empty. - - Args: - project_name (str): Name of project where to look for queried entities. - asset_ids (Iterable[Union[str, ObjectId]]): Asset ids that should - be found. - asset_names (Iterable[str]): Name assets that should be found. - parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. - archived (bool): Add also archived assets. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Cursor: Query cursor as iterable which returns asset documents matching - passed filters. - """ - - return _get_assets( - project_name, - asset_ids, - asset_names, - parent_ids, - True, - archived, - fields - ) - - -def get_archived_assets( - project_name, - asset_ids=None, - asset_names=None, - parent_ids=None, - fields=None -): - """Archived assets for specified project by passed filters. - - Passed filters (ids and names) are always combined so all conditions must - match. - - To receive all archived assets from project just keep filters empty. - - Args: - project_name (str): Name of project where to look for queried entities. - asset_ids (Iterable[Union[str, ObjectId]]): Asset ids that should - be found. - asset_names (Iterable[str]): Name assets that should be found. - parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Cursor: Query cursor as iterable which returns asset documents matching - passed filters. - """ - - return _get_assets( - project_name, asset_ids, asset_names, parent_ids, False, True, fields - ) - - -def get_asset_ids_with_subsets(project_name, asset_ids=None): - """Find out which assets have existing subsets. - - Args: - project_name (str): Name of project where to look for queried entities. - asset_ids (Iterable[Union[str, ObjectId]]): Look only for entered - asset ids. - - Returns: - Iterable[ObjectId]: Asset ids that have existing subsets. - """ - - subset_query = { - "type": "subset" - } - if asset_ids is not None: - asset_ids = convert_ids(asset_ids) - if not asset_ids: - return [] - subset_query["parent"] = {"$in": asset_ids} - - conn = get_project_connection(project_name) - result = conn.aggregate([ - { - "$match": subset_query - }, - { - "$group": { - "_id": "$parent", - "count": {"$sum": 1} - } - } - ]) - asset_ids_with_subsets = [] - for item in result: - asset_id = item["_id"] - count = item["count"] - if count > 0: - asset_ids_with_subsets.append(asset_id) - return asset_ids_with_subsets - - -def get_subset_by_id(project_name, subset_id, fields=None): - """Single subset entity data by its id. - - Args: - project_name (str): Name of project where to look for queried entities. - subset_id (Union[str, ObjectId]): Id of subset which should be found. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Subset entity data which can be reduced to - specified 'fields'. None is returned if subset with specified - filters was not found. - """ - - subset_id = convert_id(subset_id) - if not subset_id: - return None - - query_filters = {"type": "subset", "_id": subset_id} - conn = get_project_connection(project_name) - return conn.find_one(query_filters, _prepare_fields(fields)) - - -def get_subset_by_name(project_name, subset_name, asset_id, fields=None): - """Single subset entity data by its name and its version id. - - Args: - project_name (str): Name of project where to look for queried entities. - subset_name (str): Name of subset. - asset_id (Union[str, ObjectId]): Id of parent asset. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Subset entity data which can be reduced to - specified 'fields'. None is returned if subset with specified - filters was not found. - """ - if not subset_name: - return None - - asset_id = convert_id(asset_id) - if not asset_id: - return None - - query_filters = { - "type": "subset", - "name": subset_name, - "parent": asset_id - } - conn = get_project_connection(project_name) - return conn.find_one(query_filters, _prepare_fields(fields)) - - -def get_subsets( - project_name, - subset_ids=None, - subset_names=None, - asset_ids=None, - names_by_asset_ids=None, - archived=False, - fields=None -): - """Subset entities data from one project filtered by entered filters. - - Filters are additive (all conditions must pass to return subset). - - Args: - project_name (str): Name of project where to look for queried entities. - subset_ids (Iterable[Union[str, ObjectId]]): Subset ids that should be - queried. Filter ignored if 'None' is passed. - subset_names (Iterable[str]): Subset names that should be queried. - Filter ignored if 'None' is passed. - asset_ids (Iterable[Union[str, ObjectId]]): Asset ids under which - should look for the subsets. Filter ignored if 'None' is passed. - names_by_asset_ids (dict[ObjectId, List[str]]): Complex filtering - using asset ids and list of subset names under the asset. - archived (bool): Look for archived subsets too. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Cursor: Iterable cursor yielding all matching subsets. - """ - - subset_types = ["subset"] - if archived: - subset_types.append("archived_subset") - - if len(subset_types) == 1: - query_filter = {"type": subset_types[0]} - else: - query_filter = {"type": {"$in": subset_types}} - - if asset_ids is not None: - asset_ids = convert_ids(asset_ids) - if not asset_ids: - return [] - query_filter["parent"] = {"$in": asset_ids} - - if subset_ids is not None: - subset_ids = convert_ids(subset_ids) - if not subset_ids: - return [] - query_filter["_id"] = {"$in": subset_ids} - - if subset_names is not None: - if not subset_names: - return [] - query_filter["name"] = {"$in": list(subset_names)} - - if names_by_asset_ids is not None: - or_query = [] - for asset_id, names in names_by_asset_ids.items(): - if asset_id and names: - or_query.append({ - "parent": convert_id(asset_id), - "name": {"$in": list(names)} - }) - if not or_query: - return [] - query_filter["$or"] = or_query - - conn = get_project_connection(project_name) - return conn.find(query_filter, _prepare_fields(fields)) - - -def get_subset_families(project_name, subset_ids=None): - """Set of main families of subsets. - - Args: - project_name (str): Name of project where to look for queried entities. - subset_ids (Iterable[Union[str, ObjectId]]): Subset ids that should - be queried. All subsets from project are used if 'None' is passed. - - Returns: - set[str]: Main families of matching subsets. - """ - - subset_filter = { - "type": "subset" - } - if subset_ids is not None: - if not subset_ids: - return set() - subset_filter["_id"] = {"$in": list(subset_ids)} - - conn = get_project_connection(project_name) - result = list(conn.aggregate([ - {"$match": subset_filter}, - {"$project": { - "family": {"$arrayElemAt": ["$data.families", 0]} - }}, - {"$group": { - "_id": "family_group", - "families": {"$addToSet": "$family"} - }} - ])) - if result: - return set(result[0]["families"]) - return set() - - -def get_version_by_id(project_name, version_id, fields=None): - """Single version entity data by its id. - - Args: - project_name (str): Name of project where to look for queried entities. - version_id (Union[str, ObjectId]): Id of version which should be found. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Version entity data which can be reduced to - specified 'fields'. None is returned if version with specified - filters was not found. - """ - - version_id = convert_id(version_id) - if not version_id: - return None - - query_filter = { - "type": {"$in": ["version", "hero_version"]}, - "_id": version_id - } - conn = get_project_connection(project_name) - return conn.find_one(query_filter, _prepare_fields(fields)) - - -def get_version_by_name(project_name, version, subset_id, fields=None): - """Single version entity data by its name and subset id. - - Args: - project_name (str): Name of project where to look for queried entities. - version (int): name of version entity (its version). - subset_id (Union[str, ObjectId]): Id of version which should be found. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Version entity data which can be reduced to - specified 'fields'. None is returned if version with specified - filters was not found. - """ - - subset_id = convert_id(subset_id) - if not subset_id: - return None - - conn = get_project_connection(project_name) - query_filter = { - "type": "version", - "parent": subset_id, - "name": version - } - return conn.find_one(query_filter, _prepare_fields(fields)) - - -def version_is_latest(project_name, version_id): - """Is version the latest from its subset. - - Note: - Hero versions are considered as latest. - - Todo: - Maybe raise exception when version was not found? - - Args: - project_name (str):Name of project where to look for queried entities. - version_id (Union[str, ObjectId]): Version id which is checked. - - Returns: - bool: True if is latest version from subset else False. - """ - - version_id = convert_id(version_id) - if not version_id: - return False - version_doc = get_version_by_id( - project_name, version_id, fields=["_id", "type", "parent"] - ) - # What to do when version is not found? - if not version_doc: - return False - - if version_doc["type"] == "hero_version": - return True - - last_version = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] - ) - return last_version["_id"] == version_id - - -def _get_versions( - project_name, - subset_ids=None, - version_ids=None, - versions=None, - standard=True, - hero=False, - fields=None -): - version_types = [] - if standard: - version_types.append("version") - - if hero: - version_types.append("hero_version") - - if not version_types: - return [] - elif len(version_types) == 1: - query_filter = {"type": version_types[0]} - else: - query_filter = {"type": {"$in": version_types}} - - if subset_ids is not None: - subset_ids = convert_ids(subset_ids) - if not subset_ids: - return [] - query_filter["parent"] = {"$in": subset_ids} - - if version_ids is not None: - version_ids = convert_ids(version_ids) - if not version_ids: - return [] - query_filter["_id"] = {"$in": version_ids} - - if versions is not None: - versions = list(versions) - if not versions: - return [] - - if len(versions) == 1: - query_filter["name"] = versions[0] - else: - query_filter["name"] = {"$in": versions} - - conn = get_project_connection(project_name) - - return conn.find(query_filter, _prepare_fields(fields)) - - -def get_versions( - project_name, - version_ids=None, - subset_ids=None, - versions=None, - hero=False, - fields=None -): - """Version entities data from one project filtered by entered filters. - - Filters are additive (all conditions must pass to return subset). - - Args: - project_name (str): Name of project where to look for queried entities. - version_ids (Iterable[Union[str, ObjectId]]): Version ids that will - be queried. Filter ignored if 'None' is passed. - subset_ids (Iterable[str]): Subset ids that will be queried. - Filter ignored if 'None' is passed. - versions (Iterable[int]): Version names (as integers). - Filter ignored if 'None' is passed. - hero (bool): Look also for hero versions. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Cursor: Iterable cursor yielding all matching versions. - """ - - return _get_versions( - project_name, - subset_ids, - version_ids, - versions, - standard=True, - hero=hero, - fields=fields - ) - - -def get_hero_version_by_subset_id(project_name, subset_id, fields=None): - """Hero version by subset id. - - Args: - project_name (str): Name of project where to look for queried entities. - subset_id (Union[str, ObjectId]): Subset id under which - is hero version. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Hero version entity data which can be reduced to - specified 'fields'. None is returned if hero version with specified - filters was not found. - """ - - subset_id = convert_id(subset_id) - if not subset_id: - return None - - versions = list(_get_versions( - project_name, - subset_ids=[subset_id], - standard=False, - hero=True, - fields=fields - )) - if versions: - return versions[0] - return None - - -def get_hero_version_by_id(project_name, version_id, fields=None): - """Hero version by its id. - - Args: - project_name (str): Name of project where to look for queried entities. - version_id (Union[str, ObjectId]): Hero version id. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Hero version entity data which can be reduced to - specified 'fields'. None is returned if hero version with specified - filters was not found. - """ - - version_id = convert_id(version_id) - if not version_id: - return None - - versions = list(_get_versions( - project_name, - version_ids=[version_id], - standard=False, - hero=True, - fields=fields - )) - if versions: - return versions[0] - return None - - -def get_hero_versions( - project_name, - subset_ids=None, - version_ids=None, - fields=None -): - """Hero version entities data from one project filtered by entered filters. - - Args: - project_name (str): Name of project where to look for queried entities. - subset_ids (Iterable[Union[str, ObjectId]]): Subset ids for which - should look for hero versions. Filter ignored if 'None' is passed. - version_ids (Iterable[Union[str, ObjectId]]): Hero version ids. Filter - ignored if 'None' is passed. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Cursor|list: Iterable yielding hero versions matching passed filters. - """ - - return _get_versions( - project_name, - subset_ids, - version_ids, - standard=False, - hero=True, - fields=fields - ) - - -def get_output_link_versions(project_name, version_id, fields=None): - """Versions where passed version was used as input. - - Question: - Not 100% sure about the usage of the function so the name and docstring - maybe does not match what it does? - - Args: - project_name (str): Name of project where to look for queried entities. - version_id (Union[str, ObjectId]): Version id which can be used - as input link for other versions. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Iterable: Iterable cursor yielding versions that are used as input - links for passed version. - """ - - version_id = convert_id(version_id) - if not version_id: - return [] - - conn = get_project_connection(project_name) - # Does make sense to look for hero versions? - query_filter = { - "type": "version", - "data.inputLinks.id": version_id - } - return conn.find(query_filter, _prepare_fields(fields)) - - -def get_last_versions(project_name, subset_ids, active=None, fields=None): - """Latest versions for entered subset_ids. - - Args: - project_name (str): Name of project where to look for queried entities. - subset_ids (Iterable[Union[str, ObjectId]]): List of subset ids. - active (Optional[bool]): If True only active versions are returned. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - dict[ObjectId, int]: Key is subset id and value is last version name. - """ - - subset_ids = convert_ids(subset_ids) - if not subset_ids: - return {} - - if fields is not None: - fields = list(fields) - if not fields: - return {} - - # Avoid double query if only name and _id are requested - name_needed = False - limit_query = False - if fields: - fields_s = set(fields) - if "name" in fields_s: - name_needed = True - fields_s.remove("name") - - for field in ("_id", "parent"): - if field in fields_s: - fields_s.remove(field) - limit_query = len(fields_s) == 0 - - group_item = { - "_id": "$parent", - "_version_id": {"$last": "$_id"} - } - # Add name if name is needed (only for limit query) - if name_needed: - group_item["name"] = {"$last": "$name"} - - aggregate_filter = { - "type": "version", - "parent": {"$in": subset_ids} - } - if active is False: - aggregate_filter["data.active"] = active - elif active is True: - aggregate_filter["$or"] = [ - {"data.active": {"$exists": 0}}, - {"data.active": active}, - ] - - aggregation_pipeline = [ - # Find all versions of those subsets - {"$match": aggregate_filter}, - # Sorting versions all together - {"$sort": {"name": 1}}, - # Group them by "parent", but only take the last - {"$group": group_item} - ] - - conn = get_project_connection(project_name) - aggregate_result = conn.aggregate(aggregation_pipeline) - if limit_query: - output = {} - for item in aggregate_result: - subset_id = item["_id"] - item_data = {"_id": item["_version_id"], "parent": subset_id} - if name_needed: - item_data["name"] = item["name"] - output[subset_id] = item_data - return output - - version_ids = [ - doc["_version_id"] - for doc in aggregate_result - ] - - fields = _prepare_fields(fields, ["parent"]) - - version_docs = get_versions( - project_name, version_ids=version_ids, fields=fields - ) - - return { - version_doc["parent"]: version_doc - for version_doc in version_docs - } - - -def get_last_version_by_subset_id(project_name, subset_id, fields=None): - """Last version for passed subset id. - - Args: - project_name (str): Name of project where to look for queried entities. - subset_id (Union[str, ObjectId]): Id of version which should be found. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Version entity data which can be reduced to - specified 'fields'. None is returned if version with specified - filters was not found. - """ - - subset_id = convert_id(subset_id) - if not subset_id: - return None - - last_versions = get_last_versions( - project_name, subset_ids=[subset_id], fields=fields - ) - return last_versions.get(subset_id) - - -def get_last_version_by_subset_name( - project_name, subset_name, asset_id=None, asset_name=None, fields=None -): - """Last version for passed subset name under asset id/name. - - It is required to pass 'asset_id' or 'asset_name'. Asset id is recommended - if is available. - - Args: - project_name (str): Name of project where to look for queried entities. - subset_name (str): Name of subset. - asset_id (Union[str, ObjectId]): Asset id which is parent of passed - subset name. - asset_name (str): Asset name which is parent of passed subset name. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Version entity data which can be reduced to - specified 'fields'. None is returned if version with specified - filters was not found. - """ - - if not asset_id and not asset_name: - return None - - if not asset_id: - asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) - if not asset_doc: - return None - asset_id = asset_doc["_id"] - subset_doc = get_subset_by_name( - project_name, subset_name, asset_id, fields=["_id"] - ) - if not subset_doc: - return None - return get_last_version_by_subset_id( - project_name, subset_doc["_id"], fields=fields - ) - - -def get_representation_by_id(project_name, representation_id, fields=None): - """Representation entity data by its id. - - Args: - project_name (str): Name of project where to look for queried entities. - representation_id (Union[str, ObjectId]): Representation id. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Representation entity data which can be reduced to - specified 'fields'. None is returned if representation with - specified filters was not found. - """ - - if not representation_id: - return None - - repre_types = ["representation", "archived_representation"] - query_filter = { - "type": {"$in": repre_types} - } - if representation_id is not None: - query_filter["_id"] = convert_id(representation_id) - - conn = get_project_connection(project_name) - - return conn.find_one(query_filter, _prepare_fields(fields)) - - -def get_representation_by_name( - project_name, representation_name, version_id, fields=None -): - """Representation entity data by its name and its version id. - - Args: - project_name (str): Name of project where to look for queried entities. - representation_name (str): Representation name. - version_id (Union[str, ObjectId]): Id of parent version entity. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[dict[str, Any], None]: Representation entity data which can be - reduced to specified 'fields'. None is returned if representation - with specified filters was not found. - """ - - version_id = convert_id(version_id) - if not version_id or not representation_name: - return None - repre_types = ["representation", "archived_representations"] - query_filter = { - "type": {"$in": repre_types}, - "name": representation_name, - "parent": version_id - } - - conn = get_project_connection(project_name) - return conn.find_one(query_filter, _prepare_fields(fields)) - - -def _flatten_dict(data): - flatten_queue = collections.deque() - flatten_queue.append(data) - output = {} - while flatten_queue: - item = flatten_queue.popleft() - for key, value in item.items(): - if not isinstance(value, dict): - output[key] = value - continue - - tmp = {} - for subkey, subvalue in value.items(): - new_key = "{}.{}".format(key, subkey) - tmp[new_key] = subvalue - flatten_queue.append(tmp) - return output - - -def _regex_filters(filters): - output = [] - for key, value in filters.items(): - regexes = [] - a_values = [] - if isinstance(value, PatternType): - regexes.append(value) - elif isinstance(value, (list, tuple, set)): - for item in value: - if isinstance(item, PatternType): - regexes.append(item) - else: - a_values.append(item) - else: - a_values.append(value) - - key_filters = [] - if len(a_values) == 1: - key_filters.append({key: a_values[0]}) - elif a_values: - key_filters.append({key: {"$in": a_values}}) - - for regex in regexes: - key_filters.append({key: {"$regex": regex}}) - - if len(key_filters) == 1: - output.append(key_filters[0]) - else: - output.append({"$or": key_filters}) - - return output - - -def _get_representations( - project_name, - representation_ids, - representation_names, - version_ids, - context_filters, - names_by_version_ids, - standard, - archived, - fields -): - default_output = [] - repre_types = [] - if standard: - repre_types.append("representation") - if archived: - repre_types.append("archived_representation") - - if not repre_types: - return default_output - - if len(repre_types) == 1: - query_filter = {"type": repre_types[0]} - else: - query_filter = {"type": {"$in": repre_types}} - - if representation_ids is not None: - representation_ids = convert_ids(representation_ids) - if not representation_ids: - return default_output - query_filter["_id"] = {"$in": representation_ids} - - if representation_names is not None: - if not representation_names: - return default_output - query_filter["name"] = {"$in": list(representation_names)} - - if version_ids is not None: - version_ids = convert_ids(version_ids) - if not version_ids: - return default_output - query_filter["parent"] = {"$in": version_ids} - - or_queries = [] - if names_by_version_ids is not None: - or_query = [] - for version_id, names in names_by_version_ids.items(): - if version_id and names: - or_query.append({ - "parent": convert_id(version_id), - "name": {"$in": list(names)} - }) - if not or_query: - return default_output - or_queries.append(or_query) - - if context_filters is not None: - if not context_filters: - return [] - _flatten_filters = _flatten_dict(context_filters) - flatten_filters = {} - for key, value in _flatten_filters.items(): - if not key.startswith("context"): - key = "context.{}".format(key) - flatten_filters[key] = value - - for item in _regex_filters(flatten_filters): - for key, value in item.items(): - if key != "$or": - query_filter[key] = value - - elif value: - or_queries.append(value) - - if len(or_queries) == 1: - query_filter["$or"] = or_queries[0] - elif or_queries: - and_query = [] - for or_query in or_queries: - if isinstance(or_query, list): - or_query = {"$or": or_query} - and_query.append(or_query) - query_filter["$and"] = and_query - - conn = get_project_connection(project_name) - - return conn.find(query_filter, _prepare_fields(fields)) - - -def get_representations( - project_name, - representation_ids=None, - representation_names=None, - version_ids=None, - context_filters=None, - names_by_version_ids=None, - archived=False, - standard=True, - fields=None -): - """Representation entities data from one project filtered by filters. - - Filters are additive (all conditions must pass to return subset). - - Args: - project_name (str): Name of project where to look for queried entities. - representation_ids (Iterable[Union[str, ObjectId]]): Representation ids - used as filter. Filter ignored if 'None' is passed. - representation_names (Iterable[str]): Representations names used - as filter. Filter ignored if 'None' is passed. - version_ids (Iterable[str]): Subset ids used as parent filter. Filter - ignored if 'None' is passed. - context_filters (Dict[str, List[str, PatternType]]): Filter by - representation context fields. - names_by_version_ids (dict[ObjectId, list[str]]): Complex filtering - using version ids and list of names under the version. - archived (bool): Output will also contain archived representations. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Cursor: Iterable cursor yielding all matching representations. - """ - - return _get_representations( - project_name=project_name, - representation_ids=representation_ids, - representation_names=representation_names, - version_ids=version_ids, - context_filters=context_filters, - names_by_version_ids=names_by_version_ids, - standard=standard, - archived=archived, - fields=fields - ) - - -def get_archived_representations( - project_name, - representation_ids=None, - representation_names=None, - version_ids=None, - context_filters=None, - names_by_version_ids=None, - fields=None -): - """Archived representation entities data from project with applied filters. - - Filters are additive (all conditions must pass to return subset). - - Args: - project_name (str): Name of project where to look for queried entities. - representation_ids (Iterable[Union[str, ObjectId]]): Representation ids - used as filter. Filter ignored if 'None' is passed. - representation_names (Iterable[str]): Representations names used - as filter. Filter ignored if 'None' is passed. - version_ids (Iterable[str]): Subset ids used as parent filter. Filter - ignored if 'None' is passed. - context_filters (Dict[str, List[str, PatternType]]): Filter by - representation context fields. - names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering - using version ids and list of names under the version. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Cursor: Iterable cursor yielding all matching representations. - """ - - return _get_representations( - project_name=project_name, - representation_ids=representation_ids, - representation_names=representation_names, - version_ids=version_ids, - context_filters=context_filters, - names_by_version_ids=names_by_version_ids, - standard=False, - archived=True, - fields=fields - ) - - -def get_representations_parents(project_name, representations): - """Prepare parents of representation entities. - - Each item of returned dictionary contains version, subset, asset - and project in that order. - - Args: - project_name (str): Name of project where to look for queried entities. - representations (List[dict]): Representation entities with at least - '_id' and 'parent' keys. - - Returns: - dict[ObjectId, tuple]: Parents by representation id. - """ - - repre_docs_by_version_id = collections.defaultdict(list) - version_docs_by_version_id = {} - version_docs_by_subset_id = collections.defaultdict(list) - subset_docs_by_subset_id = {} - subset_docs_by_asset_id = collections.defaultdict(list) - output = {} - for repre_doc in representations: - repre_id = repre_doc["_id"] - version_id = repre_doc["parent"] - output[repre_id] = (None, None, None, None) - repre_docs_by_version_id[version_id].append(repre_doc) - - version_docs = get_versions( - project_name, - version_ids=repre_docs_by_version_id.keys(), - hero=True - ) - for version_doc in version_docs: - version_id = version_doc["_id"] - subset_id = version_doc["parent"] - version_docs_by_version_id[version_id] = version_doc - version_docs_by_subset_id[subset_id].append(version_doc) - - subset_docs = get_subsets( - project_name, subset_ids=version_docs_by_subset_id.keys() - ) - for subset_doc in subset_docs: - subset_id = subset_doc["_id"] - asset_id = subset_doc["parent"] - subset_docs_by_subset_id[subset_id] = subset_doc - subset_docs_by_asset_id[asset_id].append(subset_doc) - - asset_docs = get_assets( - project_name, asset_ids=subset_docs_by_asset_id.keys() - ) - asset_docs_by_id = { - asset_doc["_id"]: asset_doc - for asset_doc in asset_docs - } - - project_doc = get_project(project_name) - - for version_id, repre_docs in repre_docs_by_version_id.items(): - asset_doc = None - subset_doc = None - version_doc = version_docs_by_version_id.get(version_id) - if version_doc: - subset_id = version_doc["parent"] - subset_doc = subset_docs_by_subset_id.get(subset_id) - if subset_doc: - asset_id = subset_doc["parent"] - asset_doc = asset_docs_by_id.get(asset_id) - - for repre_doc in repre_docs: - repre_id = repre_doc["_id"] - output[repre_id] = ( - version_doc, subset_doc, asset_doc, project_doc - ) - return output - - -def get_representation_parents(project_name, representation): - """Prepare parents of representation entity. - - Each item of returned dictionary contains version, subset, asset - and project in that order. - - Args: - project_name (str): Name of project where to look for queried entities. - representation (dict): Representation entities with at least - '_id' and 'parent' keys. - - Returns: - dict[ObjectId, tuple]: Parents by representation id. - """ - - if not representation: - return None - - repre_id = representation["_id"] - parents_by_repre_id = get_representations_parents( - project_name, [representation] - ) - return parents_by_repre_id[repre_id] - - -def get_thumbnail_id_from_source(project_name, src_type, src_id): - """Receive thumbnail id from source entity. - - Args: - project_name (str): Name of project where to look for queried entities. - src_type (str): Type of source entity ('asset', 'version'). - src_id (Union[str, ObjectId]): Id of source entity. - - Returns: - Union[ObjectId, None]: Thumbnail id assigned to entity. If Source - entity does not have any thumbnail id assigned. - """ - - if not src_type or not src_id: - return None - - query_filter = {"_id": convert_id(src_id)} - - conn = get_project_connection(project_name) - src_doc = conn.find_one(query_filter, {"data.thumbnail_id"}) - if src_doc: - return src_doc.get("data", {}).get("thumbnail_id") - return None - - -def get_thumbnails(project_name, thumbnail_ids, fields=None): - """Receive thumbnails entity data. - - Thumbnail entity can be used to receive binary content of thumbnail based - on its content and ThumbnailResolvers. - - Args: - project_name (str): Name of project where to look for queried entities. - thumbnail_ids (Iterable[Union[str, ObjectId]]): Ids of thumbnail - entities. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - cursor: Cursor of queried documents. - """ - - if thumbnail_ids: - thumbnail_ids = convert_ids(thumbnail_ids) - - if not thumbnail_ids: - return [] - query_filter = { - "type": "thumbnail", - "_id": {"$in": thumbnail_ids} - } - conn = get_project_connection(project_name) - return conn.find(query_filter, _prepare_fields(fields)) - - -def get_thumbnail(project_name, thumbnail_id, fields=None): - """Receive thumbnail entity data. - - Args: - project_name (str): Name of project where to look for queried entities. - thumbnail_id (Union[str, ObjectId]): Id of thumbnail entity. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Thumbnail entity data which can be reduced to - specified 'fields'.None is returned if thumbnail with specified - filters was not found. - """ - - if not thumbnail_id: - return None - query_filter = {"type": "thumbnail", "_id": convert_id(thumbnail_id)} - conn = get_project_connection(project_name) - return conn.find_one(query_filter, _prepare_fields(fields)) - - -def get_workfile_info( - project_name, asset_id, task_name, filename, fields=None -): - """Document with workfile information. - - Warning: - Query is based on filename and context which does not meant it will - find always right and expected result. Information have limited usage - and is not recommended to use it as source information about workfile. - - Args: - project_name (str): Name of project where to look for queried entities. - asset_id (Union[str, ObjectId]): Id of asset entity. - task_name (str): Task name on asset. - fields (Optional[Iterable[str]]): Fields that should be returned. All - fields are returned if 'None' is passed. - - Returns: - Union[Dict, None]: Workfile entity data which can be reduced to - specified 'fields'.None is returned if workfile with specified - filters was not found. - """ - - if not asset_id or not task_name or not filename: - return None - - query_filter = { - "type": "workfile", - "parent": convert_id(asset_id), - "task_name": task_name, - "filename": filename - } - conn = get_project_connection(project_name) - return conn.find_one(query_filter, _prepare_fields(fields)) - - -""" -## Custom data storage: -- Settings - OP settings overrides and local settings -- Logging - logs from Logger -- Webpublisher - jobs -- Ftrack - events -- Maya - Shaders - - openpype/hosts/maya/api/shader_definition_editor.py - - openpype/hosts/maya/plugins/publish/validate_model_name.py - -## Global publish plugins -- openpype/plugins/publish/extract_hierarchy_avalon.py - Create: - - asset - Update: - - asset - -## Lib -- openpype/lib/avalon_context.py - Update: - - workfile data -- openpype/lib/project_backpack.py - Update: - - project -""" +if not AYON_SERVER_ENABLED: + from .mongo.entities import * +else: + from .server.entities import * diff --git a/openpype/client/entity_links.py b/openpype/client/entity_links.py index b74b4ce7f6..e18970de90 100644 --- a/openpype/client/entity_links.py +++ b/openpype/client/entity_links.py @@ -1,243 +1,6 @@ -from .mongo import get_project_connection -from .entities import ( - get_assets, - get_asset_by_id, - get_version_by_id, - get_representation_by_id, - convert_id, -) +from openpype import AYON_SERVER_ENABLED - -def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None): - """Extract linked asset ids from asset document. - - One of asset document or asset id must be passed. - - Note: - Asset links now works only from asset to assets. - - Args: - asset_doc (dict): Asset document from DB. - - Returns: - List[Union[ObjectId, str]]: Asset ids of input links. - """ - - output = [] - if not asset_doc and not asset_id: - return output - - if not asset_doc: - asset_doc = get_asset_by_id( - project_name, asset_id, fields=["data.inputLinks"] - ) - - input_links = asset_doc["data"].get("inputLinks") - if not input_links: - return output - - for item in input_links: - # Backwards compatibility for "_id" key which was replaced with - # "id" - if "_id" in item: - link_id = item["_id"] - else: - link_id = item["id"] - output.append(link_id) - return output - - -def get_linked_assets( - project_name, asset_doc=None, asset_id=None, fields=None -): - """Return linked assets based on passed asset document. - - One of asset document or asset id must be passed. - - Args: - project_name (str): Name of project where to look for queried entities. - asset_doc (Dict[str, Any]): Asset document from database. - asset_id (Union[ObjectId, str]): Asset id. Can be used instead of - asset document. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. - - Returns: - List[Dict[str, Any]]: Asset documents of input links for passed - asset doc. - """ - - if not asset_doc: - if not asset_id: - return [] - asset_doc = get_asset_by_id( - project_name, - asset_id, - fields=["data.inputLinks"] - ) - if not asset_doc: - return [] - - link_ids = get_linked_asset_ids(project_name, asset_doc=asset_doc) - if not link_ids: - return [] - - return list(get_assets(project_name, asset_ids=link_ids, fields=fields)) - - -def get_linked_representation_id( - project_name, repre_doc=None, repre_id=None, link_type=None, max_depth=None -): - """Returns list of linked ids of particular type (if provided). - - One of representation document or representation id must be passed. - Note: - Representation links now works only from representation through version - back to representations. - - Args: - project_name (str): Name of project where look for links. - repre_doc (Dict[str, Any]): Representation document. - repre_id (Union[ObjectId, str]): Representation id. - link_type (str): Type of link (e.g. 'reference', ...). - max_depth (int): Limit recursion level. Default: 0 - - Returns: - List[ObjectId] Linked representation ids. - """ - - if repre_doc: - repre_id = repre_doc["_id"] - - if repre_id: - repre_id = convert_id(repre_id) - - if not repre_id and not repre_doc: - return [] - - version_id = None - if repre_doc: - version_id = repre_doc.get("parent") - - if not version_id: - repre_doc = get_representation_by_id( - project_name, repre_id, fields=["parent"] - ) - version_id = repre_doc["parent"] - - if not version_id: - return [] - - version_doc = get_version_by_id( - project_name, version_id, fields=["type", "version_id"] - ) - if version_doc["type"] == "hero_version": - version_id = version_doc["version_id"] - - if max_depth is None: - max_depth = 0 - - match = { - "_id": version_id, - # Links are not stored to hero versions at this moment so filter - # is limited to just versions - "type": "version" - } - - graph_lookup = { - "from": project_name, - "startWith": "$data.inputLinks.id", - "connectFromField": "data.inputLinks.id", - "connectToField": "_id", - "as": "outputs_recursive", - "depthField": "depth" - } - if max_depth != 0: - # We offset by -1 since 0 basically means no recursion - # but the recursion only happens after the initial lookup - # for outputs. - graph_lookup["maxDepth"] = max_depth - 1 - - query_pipeline = [ - # Match - {"$match": match}, - # Recursive graph lookup for inputs - {"$graphLookup": graph_lookup} - ] - conn = get_project_connection(project_name) - result = conn.aggregate(query_pipeline) - referenced_version_ids = _process_referenced_pipeline_result( - result, link_type - ) - if not referenced_version_ids: - return [] - - ref_ids = conn.distinct( - "_id", - filter={ - "parent": {"$in": list(referenced_version_ids)}, - "type": "representation" - } - ) - - return list(ref_ids) - - -def _process_referenced_pipeline_result(result, link_type): - """Filters result from pipeline for particular link_type. - - Pipeline cannot use link_type directly in a query. - - Returns: - (list) - """ - - referenced_version_ids = set() - correctly_linked_ids = set() - for item in result: - input_links = item.get("data", {}).get("inputLinks") - if not input_links: - continue - - _filter_input_links( - input_links, - link_type, - correctly_linked_ids - ) - - # outputs_recursive in random order, sort by depth - outputs_recursive = item.get("outputs_recursive") - if not outputs_recursive: - continue - - for output in sorted(outputs_recursive, key=lambda o: o["depth"]): - output_links = output.get("data", {}).get("inputLinks") - if not output_links and output["type"] != "hero_version": - continue - - # Leaf - if output["_id"] not in correctly_linked_ids: - continue - - _filter_input_links( - output_links, - link_type, - correctly_linked_ids - ) - - referenced_version_ids.add(output["_id"]) - - return referenced_version_ids - - -def _filter_input_links(input_links, link_type, correctly_linked_ids): - if not input_links: # to handle hero versions - return - - for input_link in input_links: - if link_type and input_link["type"] != link_type: - continue - - link_id = input_link.get("id") or input_link.get("_id") - if link_id is not None: - correctly_linked_ids.add(link_id) +if not AYON_SERVER_ENABLED: + from .mongo.entity_links import * +else: + from .server.entity_links import * diff --git a/openpype/client/mongo/__init__.py b/openpype/client/mongo/__init__.py new file mode 100644 index 0000000000..5c5143a731 --- /dev/null +++ b/openpype/client/mongo/__init__.py @@ -0,0 +1,20 @@ +from .mongo import ( + MongoEnvNotSet, + get_default_components, + should_add_certificate_path_to_mongo_url, + validate_mongo_connection, + OpenPypeMongoConnection, + get_project_database, + get_project_connection, +) + + +__all__ = ( + "MongoEnvNotSet", + "get_default_components", + "should_add_certificate_path_to_mongo_url", + "validate_mongo_connection", + "OpenPypeMongoConnection", + "get_project_database", + "get_project_connection", +) diff --git a/openpype/client/mongo/entities.py b/openpype/client/mongo/entities.py new file mode 100644 index 0000000000..adbdd7a47c --- /dev/null +++ b/openpype/client/mongo/entities.py @@ -0,0 +1,1553 @@ +"""Unclear if these will have public functions like these. + +Goal is that most of functions here are called on (or with) an object +that has project name as a context (e.g. on 'ProjectEntity'?). + ++ We will need more specific functions doing very specific queries really fast. +""" + +import re +import collections + +import six +from bson.objectid import ObjectId + +from .mongo import get_project_database, get_project_connection + +PatternType = type(re.compile("")) + + +def _prepare_fields(fields, required_fields=None): + if not fields: + return None + + output = { + field: True + for field in fields + } + if "_id" not in output: + output["_id"] = True + + if required_fields: + for key in required_fields: + output[key] = True + return output + + +def convert_id(in_id): + """Helper function for conversion of id from string to ObjectId. + + Args: + in_id (Union[str, ObjectId, Any]): Entity id that should be converted + to right type for queries. + + Returns: + Union[ObjectId, Any]: Converted ids to ObjectId or in type. + """ + + if isinstance(in_id, six.string_types): + return ObjectId(in_id) + return in_id + + +def convert_ids(in_ids): + """Helper function for conversion of ids from string to ObjectId. + + Args: + in_ids (Iterable[Union[str, ObjectId, Any]]): List of entity ids that + should be converted to right type for queries. + + Returns: + List[ObjectId]: Converted ids to ObjectId. + """ + + _output = set() + for in_id in in_ids: + if in_id is not None: + _output.add(convert_id(in_id)) + return list(_output) + + +def get_projects(active=True, inactive=False, fields=None): + """Yield all project entity documents. + + Args: + active (Optional[bool]): Include active projects. Defaults to True. + inactive (Optional[bool]): Include inactive projects. + Defaults to False. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Yields: + dict: Project entity data which can be reduced to specified 'fields'. + None is returned if project with specified filters was not found. + """ + mongodb = get_project_database() + for project_name in mongodb.collection_names(): + if project_name in ("system.indexes",): + continue + project_doc = get_project( + project_name, active=active, inactive=inactive, fields=fields + ) + if project_doc is not None: + yield project_doc + + +def get_project(project_name, active=True, inactive=True, fields=None): + """Return project entity document by project name. + + Args: + project_name (str): Name of project. + active (Optional[bool]): Allow active project. Defaults to True. + inactive (Optional[bool]): Allow inactive project. Defaults to True. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Project entity data which can be reduced to + specified 'fields'. None is returned if project with specified + filters was not found. + """ + # Skip if both are disabled + if not active and not inactive: + return None + + query_filter = {"type": "project"} + # Keep query untouched if both should be available + if active and inactive: + pass + + # Add filter to keep only active + elif active: + query_filter["$or"] = [ + {"data.active": {"$exists": False}}, + {"data.active": True}, + ] + + # Add filter to keep only inactive + elif inactive: + query_filter["$or"] = [ + {"data.active": {"$exists": False}}, + {"data.active": False}, + ] + + conn = get_project_connection(project_name) + return conn.find_one(query_filter, _prepare_fields(fields)) + + +def get_whole_project(project_name): + """Receive all documents from project. + + Helper that can be used to get all document from whole project. For example + for backups etc. + + Returns: + Cursor: Query cursor as iterable which returns all documents from + project collection. + """ + + conn = get_project_connection(project_name) + return conn.find({}) + + +def get_asset_by_id(project_name, asset_id, fields=None): + """Receive asset data by its id. + + Args: + project_name (str): Name of project where to look for queried entities. + asset_id (Union[str, ObjectId]): Asset's id. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Asset entity data which can be reduced to + specified 'fields'. None is returned if asset with specified + filters was not found. + """ + + asset_id = convert_id(asset_id) + if not asset_id: + return None + + query_filter = {"type": "asset", "_id": asset_id} + conn = get_project_connection(project_name) + return conn.find_one(query_filter, _prepare_fields(fields)) + + +def get_asset_by_name(project_name, asset_name, fields=None): + """Receive asset data by its name. + + Args: + project_name (str): Name of project where to look for queried entities. + asset_name (str): Asset's name. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Asset entity data which can be reduced to + specified 'fields'. None is returned if asset with specified + filters was not found. + """ + + if not asset_name: + return None + + query_filter = {"type": "asset", "name": asset_name} + conn = get_project_connection(project_name) + return conn.find_one(query_filter, _prepare_fields(fields)) + + +# NOTE this could be just public function? +# - any better variable name instead of 'standard'? +# - same approach can be used for rest of types +def _get_assets( + project_name, + asset_ids=None, + asset_names=None, + parent_ids=None, + standard=True, + archived=False, + fields=None +): + """Assets for specified project by passed filters. + + Passed filters (ids and names) are always combined so all conditions must + match. + + To receive all assets from project just keep filters empty. + + Args: + project_name (str): Name of project where to look for queried entities. + asset_ids (Iterable[Union[str, ObjectId]]): Asset ids that should + be found. + asset_names (Iterable[str]): Name assets that should be found. + parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. + standard (bool): Query standard assets (type 'asset'). + archived (bool): Query archived assets (type 'archived_asset'). + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Cursor: Query cursor as iterable which returns asset documents matching + passed filters. + """ + + asset_types = [] + if standard: + asset_types.append("asset") + if archived: + asset_types.append("archived_asset") + + if not asset_types: + return [] + + if len(asset_types) == 1: + query_filter = {"type": asset_types[0]} + else: + query_filter = {"type": {"$in": asset_types}} + + if asset_ids is not None: + asset_ids = convert_ids(asset_ids) + if not asset_ids: + return [] + query_filter["_id"] = {"$in": asset_ids} + + if asset_names is not None: + if not asset_names: + return [] + query_filter["name"] = {"$in": list(asset_names)} + + if parent_ids is not None: + parent_ids = convert_ids(parent_ids) + if not parent_ids: + return [] + query_filter["data.visualParent"] = {"$in": parent_ids} + + conn = get_project_connection(project_name) + + return conn.find(query_filter, _prepare_fields(fields)) + + +def get_assets( + project_name, + asset_ids=None, + asset_names=None, + parent_ids=None, + archived=False, + fields=None +): + """Assets for specified project by passed filters. + + Passed filters (ids and names) are always combined so all conditions must + match. + + To receive all assets from project just keep filters empty. + + Args: + project_name (str): Name of project where to look for queried entities. + asset_ids (Iterable[Union[str, ObjectId]]): Asset ids that should + be found. + asset_names (Iterable[str]): Name assets that should be found. + parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. + archived (bool): Add also archived assets. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Cursor: Query cursor as iterable which returns asset documents matching + passed filters. + """ + + return _get_assets( + project_name, + asset_ids, + asset_names, + parent_ids, + True, + archived, + fields + ) + + +def get_archived_assets( + project_name, + asset_ids=None, + asset_names=None, + parent_ids=None, + fields=None +): + """Archived assets for specified project by passed filters. + + Passed filters (ids and names) are always combined so all conditions must + match. + + To receive all archived assets from project just keep filters empty. + + Args: + project_name (str): Name of project where to look for queried entities. + asset_ids (Iterable[Union[str, ObjectId]]): Asset ids that should + be found. + asset_names (Iterable[str]): Name assets that should be found. + parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Cursor: Query cursor as iterable which returns asset documents matching + passed filters. + """ + + return _get_assets( + project_name, asset_ids, asset_names, parent_ids, False, True, fields + ) + + +def get_asset_ids_with_subsets(project_name, asset_ids=None): + """Find out which assets have existing subsets. + + Args: + project_name (str): Name of project where to look for queried entities. + asset_ids (Iterable[Union[str, ObjectId]]): Look only for entered + asset ids. + + Returns: + Iterable[ObjectId]: Asset ids that have existing subsets. + """ + + subset_query = { + "type": "subset" + } + if asset_ids is not None: + asset_ids = convert_ids(asset_ids) + if not asset_ids: + return [] + subset_query["parent"] = {"$in": asset_ids} + + conn = get_project_connection(project_name) + result = conn.aggregate([ + { + "$match": subset_query + }, + { + "$group": { + "_id": "$parent", + "count": {"$sum": 1} + } + } + ]) + asset_ids_with_subsets = [] + for item in result: + asset_id = item["_id"] + count = item["count"] + if count > 0: + asset_ids_with_subsets.append(asset_id) + return asset_ids_with_subsets + + +def get_subset_by_id(project_name, subset_id, fields=None): + """Single subset entity data by its id. + + Args: + project_name (str): Name of project where to look for queried entities. + subset_id (Union[str, ObjectId]): Id of subset which should be found. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Subset entity data which can be reduced to + specified 'fields'. None is returned if subset with specified + filters was not found. + """ + + subset_id = convert_id(subset_id) + if not subset_id: + return None + + query_filters = {"type": "subset", "_id": subset_id} + conn = get_project_connection(project_name) + return conn.find_one(query_filters, _prepare_fields(fields)) + + +def get_subset_by_name(project_name, subset_name, asset_id, fields=None): + """Single subset entity data by its name and its version id. + + Args: + project_name (str): Name of project where to look for queried entities. + subset_name (str): Name of subset. + asset_id (Union[str, ObjectId]): Id of parent asset. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Subset entity data which can be reduced to + specified 'fields'. None is returned if subset with specified + filters was not found. + """ + if not subset_name: + return None + + asset_id = convert_id(asset_id) + if not asset_id: + return None + + query_filters = { + "type": "subset", + "name": subset_name, + "parent": asset_id + } + conn = get_project_connection(project_name) + return conn.find_one(query_filters, _prepare_fields(fields)) + + +def get_subsets( + project_name, + subset_ids=None, + subset_names=None, + asset_ids=None, + names_by_asset_ids=None, + archived=False, + fields=None +): + """Subset entities data from one project filtered by entered filters. + + Filters are additive (all conditions must pass to return subset). + + Args: + project_name (str): Name of project where to look for queried entities. + subset_ids (Iterable[Union[str, ObjectId]]): Subset ids that should be + queried. Filter ignored if 'None' is passed. + subset_names (Iterable[str]): Subset names that should be queried. + Filter ignored if 'None' is passed. + asset_ids (Iterable[Union[str, ObjectId]]): Asset ids under which + should look for the subsets. Filter ignored if 'None' is passed. + names_by_asset_ids (dict[ObjectId, List[str]]): Complex filtering + using asset ids and list of subset names under the asset. + archived (bool): Look for archived subsets too. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Cursor: Iterable cursor yielding all matching subsets. + """ + + subset_types = ["subset"] + if archived: + subset_types.append("archived_subset") + + if len(subset_types) == 1: + query_filter = {"type": subset_types[0]} + else: + query_filter = {"type": {"$in": subset_types}} + + if asset_ids is not None: + asset_ids = convert_ids(asset_ids) + if not asset_ids: + return [] + query_filter["parent"] = {"$in": asset_ids} + + if subset_ids is not None: + subset_ids = convert_ids(subset_ids) + if not subset_ids: + return [] + query_filter["_id"] = {"$in": subset_ids} + + if subset_names is not None: + if not subset_names: + return [] + query_filter["name"] = {"$in": list(subset_names)} + + if names_by_asset_ids is not None: + or_query = [] + for asset_id, names in names_by_asset_ids.items(): + if asset_id and names: + or_query.append({ + "parent": convert_id(asset_id), + "name": {"$in": list(names)} + }) + if not or_query: + return [] + query_filter["$or"] = or_query + + conn = get_project_connection(project_name) + return conn.find(query_filter, _prepare_fields(fields)) + + +def get_subset_families(project_name, subset_ids=None): + """Set of main families of subsets. + + Args: + project_name (str): Name of project where to look for queried entities. + subset_ids (Iterable[Union[str, ObjectId]]): Subset ids that should + be queried. All subsets from project are used if 'None' is passed. + + Returns: + set[str]: Main families of matching subsets. + """ + + subset_filter = { + "type": "subset" + } + if subset_ids is not None: + if not subset_ids: + return set() + subset_filter["_id"] = {"$in": list(subset_ids)} + + conn = get_project_connection(project_name) + result = list(conn.aggregate([ + {"$match": subset_filter}, + {"$project": { + "family": {"$arrayElemAt": ["$data.families", 0]} + }}, + {"$group": { + "_id": "family_group", + "families": {"$addToSet": "$family"} + }} + ])) + if result: + return set(result[0]["families"]) + return set() + + +def get_version_by_id(project_name, version_id, fields=None): + """Single version entity data by its id. + + Args: + project_name (str): Name of project where to look for queried entities. + version_id (Union[str, ObjectId]): Id of version which should be found. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. + """ + + version_id = convert_id(version_id) + if not version_id: + return None + + query_filter = { + "type": {"$in": ["version", "hero_version"]}, + "_id": version_id + } + conn = get_project_connection(project_name) + return conn.find_one(query_filter, _prepare_fields(fields)) + + +def get_version_by_name(project_name, version, subset_id, fields=None): + """Single version entity data by its name and subset id. + + Args: + project_name (str): Name of project where to look for queried entities. + version (int): name of version entity (its version). + subset_id (Union[str, ObjectId]): Id of version which should be found. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. + """ + + subset_id = convert_id(subset_id) + if not subset_id: + return None + + conn = get_project_connection(project_name) + query_filter = { + "type": "version", + "parent": subset_id, + "name": version + } + return conn.find_one(query_filter, _prepare_fields(fields)) + + +def version_is_latest(project_name, version_id): + """Is version the latest from its subset. + + Note: + Hero versions are considered as latest. + + Todo: + Maybe raise exception when version was not found? + + Args: + project_name (str):Name of project where to look for queried entities. + version_id (Union[str, ObjectId]): Version id which is checked. + + Returns: + bool: True if is latest version from subset else False. + """ + + version_id = convert_id(version_id) + if not version_id: + return False + version_doc = get_version_by_id( + project_name, version_id, fields=["_id", "type", "parent"] + ) + # What to do when version is not found? + if not version_doc: + return False + + if version_doc["type"] == "hero_version": + return True + + last_version = get_last_version_by_subset_id( + project_name, version_doc["parent"], fields=["_id"] + ) + return last_version["_id"] == version_id + + +def _get_versions( + project_name, + subset_ids=None, + version_ids=None, + versions=None, + standard=True, + hero=False, + fields=None +): + version_types = [] + if standard: + version_types.append("version") + + if hero: + version_types.append("hero_version") + + if not version_types: + return [] + elif len(version_types) == 1: + query_filter = {"type": version_types[0]} + else: + query_filter = {"type": {"$in": version_types}} + + if subset_ids is not None: + subset_ids = convert_ids(subset_ids) + if not subset_ids: + return [] + query_filter["parent"] = {"$in": subset_ids} + + if version_ids is not None: + version_ids = convert_ids(version_ids) + if not version_ids: + return [] + query_filter["_id"] = {"$in": version_ids} + + if versions is not None: + versions = list(versions) + if not versions: + return [] + + if len(versions) == 1: + query_filter["name"] = versions[0] + else: + query_filter["name"] = {"$in": versions} + + conn = get_project_connection(project_name) + + return conn.find(query_filter, _prepare_fields(fields)) + + +def get_versions( + project_name, + version_ids=None, + subset_ids=None, + versions=None, + hero=False, + fields=None +): + """Version entities data from one project filtered by entered filters. + + Filters are additive (all conditions must pass to return subset). + + Args: + project_name (str): Name of project where to look for queried entities. + version_ids (Iterable[Union[str, ObjectId]]): Version ids that will + be queried. Filter ignored if 'None' is passed. + subset_ids (Iterable[str]): Subset ids that will be queried. + Filter ignored if 'None' is passed. + versions (Iterable[int]): Version names (as integers). + Filter ignored if 'None' is passed. + hero (bool): Look also for hero versions. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Cursor: Iterable cursor yielding all matching versions. + """ + + return _get_versions( + project_name, + subset_ids, + version_ids, + versions, + standard=True, + hero=hero, + fields=fields + ) + + +def get_hero_version_by_subset_id(project_name, subset_id, fields=None): + """Hero version by subset id. + + Args: + project_name (str): Name of project where to look for queried entities. + subset_id (Union[str, ObjectId]): Subset id under which + is hero version. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Hero version entity data which can be reduced to + specified 'fields'. None is returned if hero version with specified + filters was not found. + """ + + subset_id = convert_id(subset_id) + if not subset_id: + return None + + versions = list(_get_versions( + project_name, + subset_ids=[subset_id], + standard=False, + hero=True, + fields=fields + )) + if versions: + return versions[0] + return None + + +def get_hero_version_by_id(project_name, version_id, fields=None): + """Hero version by its id. + + Args: + project_name (str): Name of project where to look for queried entities. + version_id (Union[str, ObjectId]): Hero version id. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Hero version entity data which can be reduced to + specified 'fields'. None is returned if hero version with specified + filters was not found. + """ + + version_id = convert_id(version_id) + if not version_id: + return None + + versions = list(_get_versions( + project_name, + version_ids=[version_id], + standard=False, + hero=True, + fields=fields + )) + if versions: + return versions[0] + return None + + +def get_hero_versions( + project_name, + subset_ids=None, + version_ids=None, + fields=None +): + """Hero version entities data from one project filtered by entered filters. + + Args: + project_name (str): Name of project where to look for queried entities. + subset_ids (Iterable[Union[str, ObjectId]]): Subset ids for which + should look for hero versions. Filter ignored if 'None' is passed. + version_ids (Iterable[Union[str, ObjectId]]): Hero version ids. Filter + ignored if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Cursor|list: Iterable yielding hero versions matching passed filters. + """ + + return _get_versions( + project_name, + subset_ids, + version_ids, + standard=False, + hero=True, + fields=fields + ) + + +def get_output_link_versions(project_name, version_id, fields=None): + """Versions where passed version was used as input. + + Question: + Not 100% sure about the usage of the function so the name and docstring + maybe does not match what it does? + + Args: + project_name (str): Name of project where to look for queried entities. + version_id (Union[str, ObjectId]): Version id which can be used + as input link for other versions. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Iterable: Iterable cursor yielding versions that are used as input + links for passed version. + """ + + version_id = convert_id(version_id) + if not version_id: + return [] + + conn = get_project_connection(project_name) + # Does make sense to look for hero versions? + query_filter = { + "type": "version", + "data.inputLinks.id": version_id + } + return conn.find(query_filter, _prepare_fields(fields)) + + +def get_last_versions(project_name, subset_ids, active=None, fields=None): + """Latest versions for entered subset_ids. + + Args: + project_name (str): Name of project where to look for queried entities. + subset_ids (Iterable[Union[str, ObjectId]]): List of subset ids. + active (Optional[bool]): If True only active versions are returned. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + dict[ObjectId, int]: Key is subset id and value is last version name. + """ + + subset_ids = convert_ids(subset_ids) + if not subset_ids: + return {} + + if fields is not None: + fields = list(fields) + if not fields: + return {} + + # Avoid double query if only name and _id are requested + name_needed = False + limit_query = False + if fields: + fields_s = set(fields) + if "name" in fields_s: + name_needed = True + fields_s.remove("name") + + for field in ("_id", "parent"): + if field in fields_s: + fields_s.remove(field) + limit_query = len(fields_s) == 0 + + group_item = { + "_id": "$parent", + "_version_id": {"$last": "$_id"} + } + # Add name if name is needed (only for limit query) + if name_needed: + group_item["name"] = {"$last": "$name"} + + aggregate_filter = { + "type": "version", + "parent": {"$in": subset_ids} + } + if active is False: + aggregate_filter["data.active"] = active + elif active is True: + aggregate_filter["$or"] = [ + {"data.active": {"$exists": 0}}, + {"data.active": active}, + ] + + aggregation_pipeline = [ + # Find all versions of those subsets + {"$match": aggregate_filter}, + # Sorting versions all together + {"$sort": {"name": 1}}, + # Group them by "parent", but only take the last + {"$group": group_item} + ] + + conn = get_project_connection(project_name) + aggregate_result = conn.aggregate(aggregation_pipeline) + if limit_query: + output = {} + for item in aggregate_result: + subset_id = item["_id"] + item_data = {"_id": item["_version_id"], "parent": subset_id} + if name_needed: + item_data["name"] = item["name"] + output[subset_id] = item_data + return output + + version_ids = [ + doc["_version_id"] + for doc in aggregate_result + ] + + fields = _prepare_fields(fields, ["parent"]) + + version_docs = get_versions( + project_name, version_ids=version_ids, fields=fields + ) + + return { + version_doc["parent"]: version_doc + for version_doc in version_docs + } + + +def get_last_version_by_subset_id(project_name, subset_id, fields=None): + """Last version for passed subset id. + + Args: + project_name (str): Name of project where to look for queried entities. + subset_id (Union[str, ObjectId]): Id of version which should be found. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. + """ + + subset_id = convert_id(subset_id) + if not subset_id: + return None + + last_versions = get_last_versions( + project_name, subset_ids=[subset_id], fields=fields + ) + return last_versions.get(subset_id) + + +def get_last_version_by_subset_name( + project_name, subset_name, asset_id=None, asset_name=None, fields=None +): + """Last version for passed subset name under asset id/name. + + It is required to pass 'asset_id' or 'asset_name'. Asset id is recommended + if is available. + + Args: + project_name (str): Name of project where to look for queried entities. + subset_name (str): Name of subset. + asset_id (Union[str, ObjectId]): Asset id which is parent of passed + subset name. + asset_name (str): Asset name which is parent of passed subset name. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. + """ + + if not asset_id and not asset_name: + return None + + if not asset_id: + asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) + if not asset_doc: + return None + asset_id = asset_doc["_id"] + subset_doc = get_subset_by_name( + project_name, subset_name, asset_id, fields=["_id"] + ) + if not subset_doc: + return None + return get_last_version_by_subset_id( + project_name, subset_doc["_id"], fields=fields + ) + + +def get_representation_by_id(project_name, representation_id, fields=None): + """Representation entity data by its id. + + Args: + project_name (str): Name of project where to look for queried entities. + representation_id (Union[str, ObjectId]): Representation id. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Representation entity data which can be reduced to + specified 'fields'. None is returned if representation with + specified filters was not found. + """ + + if not representation_id: + return None + + repre_types = ["representation", "archived_representation"] + query_filter = { + "type": {"$in": repre_types} + } + if representation_id is not None: + query_filter["_id"] = convert_id(representation_id) + + conn = get_project_connection(project_name) + + return conn.find_one(query_filter, _prepare_fields(fields)) + + +def get_representation_by_name( + project_name, representation_name, version_id, fields=None +): + """Representation entity data by its name and its version id. + + Args: + project_name (str): Name of project where to look for queried entities. + representation_name (str): Representation name. + version_id (Union[str, ObjectId]): Id of parent version entity. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[dict[str, Any], None]: Representation entity data which can be + reduced to specified 'fields'. None is returned if representation + with specified filters was not found. + """ + + version_id = convert_id(version_id) + if not version_id or not representation_name: + return None + repre_types = ["representation", "archived_representations"] + query_filter = { + "type": {"$in": repre_types}, + "name": representation_name, + "parent": version_id + } + + conn = get_project_connection(project_name) + return conn.find_one(query_filter, _prepare_fields(fields)) + + +def _flatten_dict(data): + flatten_queue = collections.deque() + flatten_queue.append(data) + output = {} + while flatten_queue: + item = flatten_queue.popleft() + for key, value in item.items(): + if not isinstance(value, dict): + output[key] = value + continue + + tmp = {} + for subkey, subvalue in value.items(): + new_key = "{}.{}".format(key, subkey) + tmp[new_key] = subvalue + flatten_queue.append(tmp) + return output + + +def _regex_filters(filters): + output = [] + for key, value in filters.items(): + regexes = [] + a_values = [] + if isinstance(value, PatternType): + regexes.append(value) + elif isinstance(value, (list, tuple, set)): + for item in value: + if isinstance(item, PatternType): + regexes.append(item) + else: + a_values.append(item) + else: + a_values.append(value) + + key_filters = [] + if len(a_values) == 1: + key_filters.append({key: a_values[0]}) + elif a_values: + key_filters.append({key: {"$in": a_values}}) + + for regex in regexes: + key_filters.append({key: {"$regex": regex}}) + + if len(key_filters) == 1: + output.append(key_filters[0]) + else: + output.append({"$or": key_filters}) + + return output + + +def _get_representations( + project_name, + representation_ids, + representation_names, + version_ids, + context_filters, + names_by_version_ids, + standard, + archived, + fields +): + default_output = [] + repre_types = [] + if standard: + repre_types.append("representation") + if archived: + repre_types.append("archived_representation") + + if not repre_types: + return default_output + + if len(repre_types) == 1: + query_filter = {"type": repre_types[0]} + else: + query_filter = {"type": {"$in": repre_types}} + + if representation_ids is not None: + representation_ids = convert_ids(representation_ids) + if not representation_ids: + return default_output + query_filter["_id"] = {"$in": representation_ids} + + if representation_names is not None: + if not representation_names: + return default_output + query_filter["name"] = {"$in": list(representation_names)} + + if version_ids is not None: + version_ids = convert_ids(version_ids) + if not version_ids: + return default_output + query_filter["parent"] = {"$in": version_ids} + + or_queries = [] + if names_by_version_ids is not None: + or_query = [] + for version_id, names in names_by_version_ids.items(): + if version_id and names: + or_query.append({ + "parent": convert_id(version_id), + "name": {"$in": list(names)} + }) + if not or_query: + return default_output + or_queries.append(or_query) + + if context_filters is not None: + if not context_filters: + return [] + _flatten_filters = _flatten_dict(context_filters) + flatten_filters = {} + for key, value in _flatten_filters.items(): + if not key.startswith("context"): + key = "context.{}".format(key) + flatten_filters[key] = value + + for item in _regex_filters(flatten_filters): + for key, value in item.items(): + if key != "$or": + query_filter[key] = value + + elif value: + or_queries.append(value) + + if len(or_queries) == 1: + query_filter["$or"] = or_queries[0] + elif or_queries: + and_query = [] + for or_query in or_queries: + if isinstance(or_query, list): + or_query = {"$or": or_query} + and_query.append(or_query) + query_filter["$and"] = and_query + + conn = get_project_connection(project_name) + + return conn.find(query_filter, _prepare_fields(fields)) + + +def get_representations( + project_name, + representation_ids=None, + representation_names=None, + version_ids=None, + context_filters=None, + names_by_version_ids=None, + archived=False, + standard=True, + fields=None +): + """Representation entities data from one project filtered by filters. + + Filters are additive (all conditions must pass to return subset). + + Args: + project_name (str): Name of project where to look for queried entities. + representation_ids (Iterable[Union[str, ObjectId]]): Representation ids + used as filter. Filter ignored if 'None' is passed. + representation_names (Iterable[str]): Representations names used + as filter. Filter ignored if 'None' is passed. + version_ids (Iterable[str]): Subset ids used as parent filter. Filter + ignored if 'None' is passed. + context_filters (Dict[str, List[str, PatternType]]): Filter by + representation context fields. + names_by_version_ids (dict[ObjectId, list[str]]): Complex filtering + using version ids and list of names under the version. + archived (bool): Output will also contain archived representations. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Cursor: Iterable cursor yielding all matching representations. + """ + + return _get_representations( + project_name=project_name, + representation_ids=representation_ids, + representation_names=representation_names, + version_ids=version_ids, + context_filters=context_filters, + names_by_version_ids=names_by_version_ids, + standard=standard, + archived=archived, + fields=fields + ) + + +def get_archived_representations( + project_name, + representation_ids=None, + representation_names=None, + version_ids=None, + context_filters=None, + names_by_version_ids=None, + fields=None +): + """Archived representation entities data from project with applied filters. + + Filters are additive (all conditions must pass to return subset). + + Args: + project_name (str): Name of project where to look for queried entities. + representation_ids (Iterable[Union[str, ObjectId]]): Representation ids + used as filter. Filter ignored if 'None' is passed. + representation_names (Iterable[str]): Representations names used + as filter. Filter ignored if 'None' is passed. + version_ids (Iterable[str]): Subset ids used as parent filter. Filter + ignored if 'None' is passed. + context_filters (Dict[str, List[str, PatternType]]): Filter by + representation context fields. + names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering + using version ids and list of names under the version. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Cursor: Iterable cursor yielding all matching representations. + """ + + return _get_representations( + project_name=project_name, + representation_ids=representation_ids, + representation_names=representation_names, + version_ids=version_ids, + context_filters=context_filters, + names_by_version_ids=names_by_version_ids, + standard=False, + archived=True, + fields=fields + ) + + +def get_representations_parents(project_name, representations): + """Prepare parents of representation entities. + + Each item of returned dictionary contains version, subset, asset + and project in that order. + + Args: + project_name (str): Name of project where to look for queried entities. + representations (List[dict]): Representation entities with at least + '_id' and 'parent' keys. + + Returns: + dict[ObjectId, tuple]: Parents by representation id. + """ + + repre_docs_by_version_id = collections.defaultdict(list) + version_docs_by_version_id = {} + version_docs_by_subset_id = collections.defaultdict(list) + subset_docs_by_subset_id = {} + subset_docs_by_asset_id = collections.defaultdict(list) + output = {} + for repre_doc in representations: + repre_id = repre_doc["_id"] + version_id = repre_doc["parent"] + output[repre_id] = (None, None, None, None) + repre_docs_by_version_id[version_id].append(repre_doc) + + version_docs = get_versions( + project_name, + version_ids=repre_docs_by_version_id.keys(), + hero=True + ) + for version_doc in version_docs: + version_id = version_doc["_id"] + subset_id = version_doc["parent"] + version_docs_by_version_id[version_id] = version_doc + version_docs_by_subset_id[subset_id].append(version_doc) + + subset_docs = get_subsets( + project_name, subset_ids=version_docs_by_subset_id.keys() + ) + for subset_doc in subset_docs: + subset_id = subset_doc["_id"] + asset_id = subset_doc["parent"] + subset_docs_by_subset_id[subset_id] = subset_doc + subset_docs_by_asset_id[asset_id].append(subset_doc) + + asset_docs = get_assets( + project_name, asset_ids=subset_docs_by_asset_id.keys() + ) + asset_docs_by_id = { + asset_doc["_id"]: asset_doc + for asset_doc in asset_docs + } + + project_doc = get_project(project_name) + + for version_id, repre_docs in repre_docs_by_version_id.items(): + asset_doc = None + subset_doc = None + version_doc = version_docs_by_version_id.get(version_id) + if version_doc: + subset_id = version_doc["parent"] + subset_doc = subset_docs_by_subset_id.get(subset_id) + if subset_doc: + asset_id = subset_doc["parent"] + asset_doc = asset_docs_by_id.get(asset_id) + + for repre_doc in repre_docs: + repre_id = repre_doc["_id"] + output[repre_id] = ( + version_doc, subset_doc, asset_doc, project_doc + ) + return output + + +def get_representation_parents(project_name, representation): + """Prepare parents of representation entity. + + Each item of returned dictionary contains version, subset, asset + and project in that order. + + Args: + project_name (str): Name of project where to look for queried entities. + representation (dict): Representation entities with at least + '_id' and 'parent' keys. + + Returns: + dict[ObjectId, tuple]: Parents by representation id. + """ + + if not representation: + return None + + repre_id = representation["_id"] + parents_by_repre_id = get_representations_parents( + project_name, [representation] + ) + return parents_by_repre_id[repre_id] + + +def get_thumbnail_id_from_source(project_name, src_type, src_id): + """Receive thumbnail id from source entity. + + Args: + project_name (str): Name of project where to look for queried entities. + src_type (str): Type of source entity ('asset', 'version'). + src_id (Union[str, ObjectId]): Id of source entity. + + Returns: + Union[ObjectId, None]: Thumbnail id assigned to entity. If Source + entity does not have any thumbnail id assigned. + """ + + if not src_type or not src_id: + return None + + query_filter = {"_id": convert_id(src_id)} + + conn = get_project_connection(project_name) + src_doc = conn.find_one(query_filter, {"data.thumbnail_id"}) + if src_doc: + return src_doc.get("data", {}).get("thumbnail_id") + return None + + +def get_thumbnails(project_name, thumbnail_ids, fields=None): + """Receive thumbnails entity data. + + Thumbnail entity can be used to receive binary content of thumbnail based + on its content and ThumbnailResolvers. + + Args: + project_name (str): Name of project where to look for queried entities. + thumbnail_ids (Iterable[Union[str, ObjectId]]): Ids of thumbnail + entities. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + cursor: Cursor of queried documents. + """ + + if thumbnail_ids: + thumbnail_ids = convert_ids(thumbnail_ids) + + if not thumbnail_ids: + return [] + query_filter = { + "type": "thumbnail", + "_id": {"$in": thumbnail_ids} + } + conn = get_project_connection(project_name) + return conn.find(query_filter, _prepare_fields(fields)) + + +def get_thumbnail(project_name, thumbnail_id, fields=None): + """Receive thumbnail entity data. + + Args: + project_name (str): Name of project where to look for queried entities. + thumbnail_id (Union[str, ObjectId]): Id of thumbnail entity. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Thumbnail entity data which can be reduced to + specified 'fields'.None is returned if thumbnail with specified + filters was not found. + """ + + if not thumbnail_id: + return None + query_filter = {"type": "thumbnail", "_id": convert_id(thumbnail_id)} + conn = get_project_connection(project_name) + return conn.find_one(query_filter, _prepare_fields(fields)) + + +def get_workfile_info( + project_name, asset_id, task_name, filename, fields=None +): + """Document with workfile information. + + Warning: + Query is based on filename and context which does not meant it will + find always right and expected result. Information have limited usage + and is not recommended to use it as source information about workfile. + + Args: + project_name (str): Name of project where to look for queried entities. + asset_id (Union[str, ObjectId]): Id of asset entity. + task_name (str): Task name on asset. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Workfile entity data which can be reduced to + specified 'fields'.None is returned if workfile with specified + filters was not found. + """ + + if not asset_id or not task_name or not filename: + return None + + query_filter = { + "type": "workfile", + "parent": convert_id(asset_id), + "task_name": task_name, + "filename": filename + } + conn = get_project_connection(project_name) + return conn.find_one(query_filter, _prepare_fields(fields)) + + +""" +## Custom data storage: +- Settings - OP settings overrides and local settings +- Logging - logs from Logger +- Webpublisher - jobs +- Ftrack - events +- Maya - Shaders + - openpype/hosts/maya/api/shader_definition_editor.py + - openpype/hosts/maya/plugins/publish/validate_model_name.py + +## Global publish plugins +- openpype/plugins/publish/extract_hierarchy_avalon.py + Create: + - asset + Update: + - asset + +## Lib +- openpype/lib/avalon_context.py + Update: + - workfile data +- openpype/lib/project_backpack.py + Update: + - project +""" diff --git a/openpype/client/mongo/entity_links.py b/openpype/client/mongo/entity_links.py new file mode 100644 index 0000000000..c97a828118 --- /dev/null +++ b/openpype/client/mongo/entity_links.py @@ -0,0 +1,244 @@ +from .mongo import get_project_connection +from .entities import ( + get_assets, + get_asset_by_id, + get_version_by_id, + get_representation_by_id, + convert_id, +) + + +def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None): + """Extract linked asset ids from asset document. + + One of asset document or asset id must be passed. + + Note: + Asset links now works only from asset to assets. + + Args: + asset_doc (dict): Asset document from DB. + + Returns: + List[Union[ObjectId, str]]: Asset ids of input links. + """ + + output = [] + if not asset_doc and not asset_id: + return output + + if not asset_doc: + asset_doc = get_asset_by_id( + project_name, asset_id, fields=["data.inputLinks"] + ) + + input_links = asset_doc["data"].get("inputLinks") + if not input_links: + return output + + for item in input_links: + # Backwards compatibility for "_id" key which was replaced with + # "id" + if "_id" in item: + link_id = item["_id"] + else: + link_id = item["id"] + output.append(link_id) + return output + + +def get_linked_assets( + project_name, asset_doc=None, asset_id=None, fields=None +): + """Return linked assets based on passed asset document. + + One of asset document or asset id must be passed. + + Args: + project_name (str): Name of project where to look for queried entities. + asset_doc (Dict[str, Any]): Asset document from database. + asset_id (Union[ObjectId, str]): Asset id. Can be used instead of + asset document. + fields (Iterable[str]): Fields that should be returned. All fields are + returned if 'None' is passed. + + Returns: + List[Dict[str, Any]]: Asset documents of input links for passed + asset doc. + """ + + if not asset_doc: + if not asset_id: + return [] + asset_doc = get_asset_by_id( + project_name, + asset_id, + fields=["data.inputLinks"] + ) + if not asset_doc: + return [] + + link_ids = get_linked_asset_ids(project_name, asset_doc=asset_doc) + if not link_ids: + return [] + + return list(get_assets(project_name, asset_ids=link_ids, fields=fields)) + + +def get_linked_representation_id( + project_name, repre_doc=None, repre_id=None, link_type=None, max_depth=None +): + """Returns list of linked ids of particular type (if provided). + + One of representation document or representation id must be passed. + Note: + Representation links now works only from representation through version + back to representations. + + Args: + project_name (str): Name of project where look for links. + repre_doc (Dict[str, Any]): Representation document. + repre_id (Union[ObjectId, str]): Representation id. + link_type (str): Type of link (e.g. 'reference', ...). + max_depth (int): Limit recursion level. Default: 0 + + Returns: + List[ObjectId] Linked representation ids. + """ + + if repre_doc: + repre_id = repre_doc["_id"] + + if repre_id: + repre_id = convert_id(repre_id) + + if not repre_id and not repre_doc: + return [] + + version_id = None + if repre_doc: + version_id = repre_doc.get("parent") + + if not version_id: + repre_doc = get_representation_by_id( + project_name, repre_id, fields=["parent"] + ) + version_id = repre_doc["parent"] + + if not version_id: + return [] + + version_doc = get_version_by_id( + project_name, version_id, fields=["type", "version_id"] + ) + if version_doc["type"] == "hero_version": + version_id = version_doc["version_id"] + + if max_depth is None: + max_depth = 0 + + match = { + "_id": version_id, + # Links are not stored to hero versions at this moment so filter + # is limited to just versions + "type": "version" + } + + graph_lookup = { + "from": project_name, + "startWith": "$data.inputLinks.id", + "connectFromField": "data.inputLinks.id", + "connectToField": "_id", + "as": "outputs_recursive", + "depthField": "depth" + } + if max_depth != 0: + # We offset by -1 since 0 basically means no recursion + # but the recursion only happens after the initial lookup + # for outputs. + graph_lookup["maxDepth"] = max_depth - 1 + + query_pipeline = [ + # Match + {"$match": match}, + # Recursive graph lookup for inputs + {"$graphLookup": graph_lookup} + ] + + conn = get_project_connection(project_name) + result = conn.aggregate(query_pipeline) + referenced_version_ids = _process_referenced_pipeline_result( + result, link_type + ) + if not referenced_version_ids: + return [] + + ref_ids = conn.distinct( + "_id", + filter={ + "parent": {"$in": list(referenced_version_ids)}, + "type": "representation" + } + ) + + return list(ref_ids) + + +def _process_referenced_pipeline_result(result, link_type): + """Filters result from pipeline for particular link_type. + + Pipeline cannot use link_type directly in a query. + + Returns: + (list) + """ + + referenced_version_ids = set() + correctly_linked_ids = set() + for item in result: + input_links = item.get("data", {}).get("inputLinks") + if not input_links: + continue + + _filter_input_links( + input_links, + link_type, + correctly_linked_ids + ) + + # outputs_recursive in random order, sort by depth + outputs_recursive = item.get("outputs_recursive") + if not outputs_recursive: + continue + + for output in sorted(outputs_recursive, key=lambda o: o["depth"]): + output_links = output.get("data", {}).get("inputLinks") + if not output_links and output["type"] != "hero_version": + continue + + # Leaf + if output["_id"] not in correctly_linked_ids: + continue + + _filter_input_links( + output_links, + link_type, + correctly_linked_ids + ) + + referenced_version_ids.add(output["_id"]) + + return referenced_version_ids + + +def _filter_input_links(input_links, link_type, correctly_linked_ids): + if not input_links: # to handle hero versions + return + + for input_link in input_links: + if link_type and input_link["type"] != link_type: + continue + + link_id = input_link.get("id") or input_link.get("_id") + if link_id is not None: + correctly_linked_ids.add(link_id) diff --git a/openpype/client/mongo.py b/openpype/client/mongo/mongo.py similarity index 98% rename from openpype/client/mongo.py rename to openpype/client/mongo/mongo.py index 251041c028..ad85782996 100644 --- a/openpype/client/mongo.py +++ b/openpype/client/mongo/mongo.py @@ -11,6 +11,7 @@ from bson.json_util import ( CANONICAL_JSON_OPTIONS ) +from openpype import AYON_SERVER_ENABLED if sys.version_info[0] == 2: from urlparse import urlparse, parse_qs else: @@ -206,6 +207,8 @@ class OpenPypeMongoConnection: @classmethod def create_connection(cls, mongo_url, timeout=None, retry_attempts=None): + if AYON_SERVER_ENABLED: + raise RuntimeError("Created mongo connection in AYON mode") parsed = urlparse(mongo_url) # Force validation of scheme if parsed.scheme not in ["mongodb", "mongodb+srv"]: diff --git a/openpype/client/mongo/operations.py b/openpype/client/mongo/operations.py new file mode 100644 index 0000000000..3537aa4a3d --- /dev/null +++ b/openpype/client/mongo/operations.py @@ -0,0 +1,632 @@ +import re +import copy +import collections + +from bson.objectid import ObjectId +from pymongo import DeleteOne, InsertOne, UpdateOne + +from openpype.client.operations_base import ( + REMOVED_VALUE, + CreateOperation, + UpdateOperation, + DeleteOperation, + BaseOperationsSession +) +from .mongo import get_project_connection +from .entities import get_project + + +PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" +PROJECT_NAME_REGEX = re.compile( + "^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS) +) + +CURRENT_PROJECT_SCHEMA = "openpype:project-3.0" +CURRENT_PROJECT_CONFIG_SCHEMA = "openpype:config-2.0" +CURRENT_ASSET_DOC_SCHEMA = "openpype:asset-3.0" +CURRENT_SUBSET_SCHEMA = "openpype:subset-3.0" +CURRENT_VERSION_SCHEMA = "openpype:version-3.0" +CURRENT_HERO_VERSION_SCHEMA = "openpype:hero_version-1.0" +CURRENT_REPRESENTATION_SCHEMA = "openpype:representation-2.0" +CURRENT_WORKFILE_INFO_SCHEMA = "openpype:workfile-1.0" +CURRENT_THUMBNAIL_SCHEMA = "openpype:thumbnail-1.0" + + +def _create_or_convert_to_mongo_id(mongo_id): + if mongo_id is None: + return ObjectId() + return ObjectId(mongo_id) + + +def new_project_document( + project_name, project_code, config, data=None, entity_id=None +): + """Create skeleton data of project document. + + Args: + project_name (str): Name of project. Used as identifier of a project. + project_code (str): Shorter version of projet without spaces and + special characters (in most of cases). Should be also considered + as unique name across projects. + config (Dic[str, Any]): Project config consist of roots, templates, + applications and other project Anatomy related data. + data (Dict[str, Any]): Project data with information about it's + attributes (e.g. 'fps' etc.) or integration specific keys. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of project document. + """ + + if data is None: + data = {} + + data["code"] = project_code + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "name": project_name, + "type": CURRENT_PROJECT_SCHEMA, + "entity_data": data, + "config": config + } + + +def new_asset_document( + name, project_id, parent_id, parents, data=None, entity_id=None +): + """Create skeleton data of asset document. + + Args: + name (str): Is considered as unique identifier of asset in project. + project_id (Union[str, ObjectId]): Id of project doument. + parent_id (Union[str, ObjectId]): Id of parent asset. + parents (List[str]): List of parent assets names. + data (Dict[str, Any]): Asset document data. Empty dictionary is used + if not passed. Value of 'parent_id' is used to fill 'visualParent'. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of asset document. + """ + + if data is None: + data = {} + if parent_id is not None: + parent_id = ObjectId(parent_id) + data["visualParent"] = parent_id + data["parents"] = parents + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "type": "asset", + "name": name, + "parent": ObjectId(project_id), + "data": data, + "schema": CURRENT_ASSET_DOC_SCHEMA + } + + +def new_subset_document(name, family, asset_id, data=None, entity_id=None): + """Create skeleton data of subset document. + + Args: + name (str): Is considered as unique identifier of subset under asset. + family (str): Subset's family. + asset_id (Union[str, ObjectId]): Id of parent asset. + data (Dict[str, Any]): Subset document data. Empty dictionary is used + if not passed. Value of 'family' is used to fill 'family'. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of subset document. + """ + + if data is None: + data = {} + data["family"] = family + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "schema": CURRENT_SUBSET_SCHEMA, + "type": "subset", + "name": name, + "data": data, + "parent": asset_id + } + + +def new_version_doc(version, subset_id, data=None, entity_id=None): + """Create skeleton data of version document. + + Args: + version (int): Is considered as unique identifier of version + under subset. + subset_id (Union[str, ObjectId]): Id of parent subset. + data (Dict[str, Any]): Version document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version document. + """ + + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "schema": CURRENT_VERSION_SCHEMA, + "type": "version", + "name": int(version), + "parent": subset_id, + "data": data + } + + +def new_hero_version_doc(version_id, subset_id, data=None, entity_id=None): + """Create skeleton data of hero version document. + + Args: + version_id (ObjectId): Is considered as unique identifier of version + under subset. + subset_id (Union[str, ObjectId]): Id of parent subset. + data (Dict[str, Any]): Version document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version document. + """ + + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "schema": CURRENT_HERO_VERSION_SCHEMA, + "type": "hero_version", + "version_id": version_id, + "parent": subset_id, + "data": data + } + + +def new_representation_doc( + name, version_id, context, data=None, entity_id=None +): + """Create skeleton data of asset document. + + Args: + version (int): Is considered as unique identifier of version + under subset. + version_id (Union[str, ObjectId]): Id of parent version. + context (Dict[str, Any]): Representation context used for fill template + of to query. + data (Dict[str, Any]): Representation document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version document. + """ + + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "schema": CURRENT_REPRESENTATION_SCHEMA, + "type": "representation", + "parent": version_id, + "name": name, + "data": data, + + # Imprint shortcut to context for performance reasons. + "context": context + } + + +def new_thumbnail_doc(data=None, entity_id=None): + """Create skeleton data of thumbnail document. + + Args: + data (Dict[str, Any]): Thumbnail document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of thumbnail document. + """ + + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "type": "thumbnail", + "schema": CURRENT_THUMBNAIL_SCHEMA, + "data": data + } + + +def new_workfile_info_doc( + filename, asset_id, task_name, files, data=None, entity_id=None +): + """Create skeleton data of workfile info document. + + Workfile document is at this moment used primarily for artist notes. + + Args: + filename (str): Filename of workfile. + asset_id (Union[str, ObjectId]): Id of asset under which workfile live. + task_name (str): Task under which was workfile created. + files (List[str]): List of rootless filepaths related to workfile. + data (Dict[str, Any]): Additional metadata. + + Returns: + Dict[str, Any]: Skeleton of workfile info document. + """ + + if not data: + data = {} + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "type": "workfile", + "parent": ObjectId(asset_id), + "task_name": task_name, + "filename": filename, + "data": data, + "files": files + } + + +def _prepare_update_data(old_doc, new_doc, replace): + changes = {} + for key, value in new_doc.items(): + if key not in old_doc or value != old_doc[key]: + changes[key] = value + + if replace: + for key in old_doc.keys(): + if key not in new_doc: + changes[key] = REMOVED_VALUE + return changes + + +def prepare_subset_update_data(old_doc, new_doc, replace=True): + """Compare two subset documents and prepare update data. + + Based on compared values will create update data for + 'MongoUpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_version_update_data(old_doc, new_doc, replace=True): + """Compare two version documents and prepare update data. + + Based on compared values will create update data for + 'MongoUpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_hero_version_update_data(old_doc, new_doc, replace=True): + """Compare two hero version documents and prepare update data. + + Based on compared values will create update data for 'UpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_representation_update_data(old_doc, new_doc, replace=True): + """Compare two representation documents and prepare update data. + + Based on compared values will create update data for + 'MongoUpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_workfile_info_update_data(old_doc, new_doc, replace=True): + """Compare two workfile info documents and prepare update data. + + Based on compared values will create update data for + 'MongoUpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +class MongoCreateOperation(CreateOperation): + """Operation to create an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + data (Dict[str, Any]): Data of entity that will be created. + """ + + operation_name = "create" + + def __init__(self, project_name, entity_type, data): + super(MongoCreateOperation, self).__init__( + project_name, entity_type, data + ) + + if "_id" not in self._data: + self._data["_id"] = ObjectId() + else: + self._data["_id"] = ObjectId(self._data["_id"]) + + @property + def entity_id(self): + return self._data["_id"] + + def to_mongo_operation(self): + return InsertOne(copy.deepcopy(self._data)) + + +class MongoUpdateOperation(UpdateOperation): + """Operation to update an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + entity_id (Union[str, ObjectId]): Identifier of an entity. + update_data (Dict[str, Any]): Key -> value changes that will be set in + database. If value is set to 'REMOVED_VALUE' the key will be + removed. Only first level of dictionary is checked (on purpose). + """ + + operation_name = "update" + + def __init__(self, project_name, entity_type, entity_id, update_data): + super(MongoUpdateOperation, self).__init__( + project_name, entity_type, entity_id, update_data + ) + + self._entity_id = ObjectId(self._entity_id) + + def to_mongo_operation(self): + unset_data = {} + set_data = {} + for key, value in self._update_data.items(): + if value is REMOVED_VALUE: + unset_data[key] = None + else: + set_data[key] = value + + op_data = {} + if unset_data: + op_data["$unset"] = unset_data + if set_data: + op_data["$set"] = set_data + + if not op_data: + return None + + return UpdateOne( + {"_id": self.entity_id}, + op_data + ) + + +class MongoDeleteOperation(DeleteOperation): + """Operation to delete an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + entity_id (Union[str, ObjectId]): Entity id that will be removed. + """ + + operation_name = "delete" + + def __init__(self, project_name, entity_type, entity_id): + super(MongoDeleteOperation, self).__init__( + project_name, entity_type, entity_id + ) + + self._entity_id = ObjectId(self._entity_id) + + def to_mongo_operation(self): + return DeleteOne({"_id": self.entity_id}) + + +class MongoOperationsSession(BaseOperationsSession): + """Session storing operations that should happen in an order. + + At this moment does not handle anything special can be sonsidered as + stupid list of operations that will happen after each other. If creation + of same entity is there multiple times it's handled in any way and document + values are not validated. + + All operations must be related to single project. + + Args: + project_name (str): Project name to which are operations related. + """ + + def commit(self): + """Commit session operations.""" + + operations, self._operations = self._operations, [] + if not operations: + return + + operations_by_project = collections.defaultdict(list) + for operation in operations: + operations_by_project[operation.project_name].append(operation) + + for project_name, operations in operations_by_project.items(): + bulk_writes = [] + for operation in operations: + mongo_op = operation.to_mongo_operation() + if mongo_op is not None: + bulk_writes.append(mongo_op) + + if bulk_writes: + collection = get_project_connection(project_name) + collection.bulk_write(bulk_writes) + + def create_entity(self, project_name, entity_type, data): + """Fast access to 'MongoCreateOperation'. + + Returns: + MongoCreateOperation: Object of update operation. + """ + + operation = MongoCreateOperation(project_name, entity_type, data) + self.add(operation) + return operation + + def update_entity(self, project_name, entity_type, entity_id, update_data): + """Fast access to 'MongoUpdateOperation'. + + Returns: + MongoUpdateOperation: Object of update operation. + """ + + operation = MongoUpdateOperation( + project_name, entity_type, entity_id, update_data + ) + self.add(operation) + return operation + + def delete_entity(self, project_name, entity_type, entity_id): + """Fast access to 'MongoDeleteOperation'. + + Returns: + MongoDeleteOperation: Object of delete operation. + """ + + operation = MongoDeleteOperation(project_name, entity_type, entity_id) + self.add(operation) + return operation + + +def create_project( + project_name, + project_code, + library_project=False, +): + """Create project using OpenPype settings. + + This project creation function is not validating project document on + creation. It is because project document is created blindly with only + minimum required information about project which is it's name, code, type + and schema. + + Entered project name must be unique and project must not exist yet. + + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. + + Args: + project_name(str): New project name. Should be unique. + project_code(str): Project's code should be unique too. + library_project(bool): Project is library project. + + Raises: + ValueError: When project name already exists in MongoDB. + + Returns: + dict: Created project document. + """ + + from openpype.settings import ProjectSettings, SaveWarningExc + from openpype.pipeline.schema import validate + + if get_project(project_name, fields=["name"]): + raise ValueError("Project with name \"{}\" already exists".format( + project_name + )) + + if not PROJECT_NAME_REGEX.match(project_name): + raise ValueError(( + "Project name \"{}\" contain invalid characters" + ).format(project_name)) + + project_doc = { + "type": "project", + "name": project_name, + "data": { + "code": project_code, + "library_project": library_project + }, + "schema": CURRENT_PROJECT_SCHEMA + } + + op_session = MongoOperationsSession() + # Insert document with basic data + create_op = op_session.create_entity( + project_name, project_doc["type"], project_doc + ) + op_session.commit() + + # Load ProjectSettings for the project and save it to store all attributes + # and Anatomy + try: + project_settings_entity = ProjectSettings(project_name) + project_settings_entity.save() + except SaveWarningExc as exc: + print(str(exc)) + except Exception: + op_session.delete_entity( + project_name, project_doc["type"], create_op.entity_id + ) + op_session.commit() + raise + + project_doc = get_project(project_name) + + try: + # Validate created project document + validate(project_doc) + except Exception: + # Remove project if is not valid + op_session.delete_entity( + project_name, project_doc["type"], create_op.entity_id + ) + op_session.commit() + raise + + return project_doc diff --git a/openpype/client/operations.py b/openpype/client/operations.py index e8c9d28636..8bc09dffd3 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -1,797 +1,24 @@ -import re -import uuid -import copy -import collections -from abc import ABCMeta, abstractmethod, abstractproperty +from openpype import AYON_SERVER_ENABLED -import six -from bson.objectid import ObjectId -from pymongo import DeleteOne, InsertOne, UpdateOne +from .operations_base import REMOVED_VALUE +if not AYON_SERVER_ENABLED: + from .mongo.operations import * + OperationsSession = MongoOperationsSession -from .mongo import get_project_connection -from .entities import get_project - -REMOVED_VALUE = object() - -PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" -PROJECT_NAME_REGEX = re.compile( - "^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS) -) - -CURRENT_PROJECT_SCHEMA = "openpype:project-3.0" -CURRENT_PROJECT_CONFIG_SCHEMA = "openpype:config-2.0" -CURRENT_ASSET_DOC_SCHEMA = "openpype:asset-3.0" -CURRENT_SUBSET_SCHEMA = "openpype:subset-3.0" -CURRENT_VERSION_SCHEMA = "openpype:version-3.0" -CURRENT_HERO_VERSION_SCHEMA = "openpype:hero_version-1.0" -CURRENT_REPRESENTATION_SCHEMA = "openpype:representation-2.0" -CURRENT_WORKFILE_INFO_SCHEMA = "openpype:workfile-1.0" -CURRENT_THUMBNAIL_SCHEMA = "openpype:thumbnail-1.0" - - -def _create_or_convert_to_mongo_id(mongo_id): - if mongo_id is None: - return ObjectId() - return ObjectId(mongo_id) - - -def new_project_document( - project_name, project_code, config, data=None, entity_id=None -): - """Create skeleton data of project document. - - Args: - project_name (str): Name of project. Used as identifier of a project. - project_code (str): Shorter version of projet without spaces and - special characters (in most of cases). Should be also considered - as unique name across projects. - config (Dic[str, Any]): Project config consist of roots, templates, - applications and other project Anatomy related data. - data (Dict[str, Any]): Project data with information about it's - attributes (e.g. 'fps' etc.) or integration specific keys. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of project document. - """ - - if data is None: - data = {} - - data["code"] = project_code - - return { - "_id": _create_or_convert_to_mongo_id(entity_id), - "name": project_name, - "type": CURRENT_PROJECT_SCHEMA, - "entity_data": data, - "config": config - } - - -def new_asset_document( - name, project_id, parent_id, parents, data=None, entity_id=None -): - """Create skeleton data of asset document. - - Args: - name (str): Is considered as unique identifier of asset in project. - project_id (Union[str, ObjectId]): Id of project doument. - parent_id (Union[str, ObjectId]): Id of parent asset. - parents (List[str]): List of parent assets names. - data (Dict[str, Any]): Asset document data. Empty dictionary is used - if not passed. Value of 'parent_id' is used to fill 'visualParent'. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of asset document. - """ - - if data is None: - data = {} - if parent_id is not None: - parent_id = ObjectId(parent_id) - data["visualParent"] = parent_id - data["parents"] = parents - - return { - "_id": _create_or_convert_to_mongo_id(entity_id), - "type": "asset", - "name": name, - "parent": ObjectId(project_id), - "data": data, - "schema": CURRENT_ASSET_DOC_SCHEMA - } - - -def new_subset_document(name, family, asset_id, data=None, entity_id=None): - """Create skeleton data of subset document. - - Args: - name (str): Is considered as unique identifier of subset under asset. - family (str): Subset's family. - asset_id (Union[str, ObjectId]): Id of parent asset. - data (Dict[str, Any]): Subset document data. Empty dictionary is used - if not passed. Value of 'family' is used to fill 'family'. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of subset document. - """ - - if data is None: - data = {} - data["family"] = family - return { - "_id": _create_or_convert_to_mongo_id(entity_id), - "schema": CURRENT_SUBSET_SCHEMA, - "type": "subset", - "name": name, - "data": data, - "parent": asset_id - } - - -def new_version_doc(version, subset_id, data=None, entity_id=None): - """Create skeleton data of version document. - - Args: - version (int): Is considered as unique identifier of version - under subset. - subset_id (Union[str, ObjectId]): Id of parent subset. - data (Dict[str, Any]): Version document data. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of version document. - """ - - if data is None: - data = {} - - return { - "_id": _create_or_convert_to_mongo_id(entity_id), - "schema": CURRENT_VERSION_SCHEMA, - "type": "version", - "name": int(version), - "parent": subset_id, - "data": data - } - - -def new_hero_version_doc(version_id, subset_id, data=None, entity_id=None): - """Create skeleton data of hero version document. - - Args: - version_id (ObjectId): Is considered as unique identifier of version - under subset. - subset_id (Union[str, ObjectId]): Id of parent subset. - data (Dict[str, Any]): Version document data. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of version document. - """ - - if data is None: - data = {} - - return { - "_id": _create_or_convert_to_mongo_id(entity_id), - "schema": CURRENT_HERO_VERSION_SCHEMA, - "type": "hero_version", - "version_id": version_id, - "parent": subset_id, - "data": data - } - - -def new_representation_doc( - name, version_id, context, data=None, entity_id=None -): - """Create skeleton data of asset document. - - Args: - version (int): Is considered as unique identifier of version - under subset. - version_id (Union[str, ObjectId]): Id of parent version. - context (Dict[str, Any]): Representation context used for fill template - of to query. - data (Dict[str, Any]): Representation document data. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of version document. - """ - - if data is None: - data = {} - - return { - "_id": _create_or_convert_to_mongo_id(entity_id), - "schema": CURRENT_REPRESENTATION_SCHEMA, - "type": "representation", - "parent": version_id, - "name": name, - "data": data, - # Imprint shortcut to context for performance reasons. - "context": context - } - - -def new_thumbnail_doc(data=None, entity_id=None): - """Create skeleton data of thumbnail document. - - Args: - data (Dict[str, Any]): Thumbnail document data. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of thumbnail document. - """ - - if data is None: - data = {} - - return { - "_id": _create_or_convert_to_mongo_id(entity_id), - "type": "thumbnail", - "schema": CURRENT_THUMBNAIL_SCHEMA, - "data": data - } - - -def new_workfile_info_doc( - filename, asset_id, task_name, files, data=None, entity_id=None -): - """Create skeleton data of workfile info document. - - Workfile document is at this moment used primarily for artist notes. - - Args: - filename (str): Filename of workfile. - asset_id (Union[str, ObjectId]): Id of asset under which workfile live. - task_name (str): Task under which was workfile created. - files (List[str]): List of rootless filepaths related to workfile. - data (Dict[str, Any]): Additional metadata. - - Returns: - Dict[str, Any]: Skeleton of workfile info document. - """ - - if not data: - data = {} - - return { - "_id": _create_or_convert_to_mongo_id(entity_id), - "type": "workfile", - "parent": ObjectId(asset_id), - "task_name": task_name, - "filename": filename, - "data": data, - "files": files - } - - -def _prepare_update_data(old_doc, new_doc, replace): - changes = {} - for key, value in new_doc.items(): - if key not in old_doc or value != old_doc[key]: - changes[key] = value - - if replace: - for key in old_doc.keys(): - if key not in new_doc: - changes[key] = REMOVED_VALUE - return changes - - -def prepare_subset_update_data(old_doc, new_doc, replace=True): - """Compare two subset documents and prepare update data. - - Based on compared values will create update data for 'UpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - return _prepare_update_data(old_doc, new_doc, replace) - - -def prepare_version_update_data(old_doc, new_doc, replace=True): - """Compare two version documents and prepare update data. - - Based on compared values will create update data for 'UpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - return _prepare_update_data(old_doc, new_doc, replace) - - -def prepare_hero_version_update_data(old_doc, new_doc, replace=True): - """Compare two hero version documents and prepare update data. - - Based on compared values will create update data for 'UpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - return _prepare_update_data(old_doc, new_doc, replace) - - -def prepare_representation_update_data(old_doc, new_doc, replace=True): - """Compare two representation documents and prepare update data. - - Based on compared values will create update data for 'UpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - return _prepare_update_data(old_doc, new_doc, replace) - - -def prepare_workfile_info_update_data(old_doc, new_doc, replace=True): - """Compare two workfile info documents and prepare update data. - - Based on compared values will create update data for 'UpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - return _prepare_update_data(old_doc, new_doc, replace) - - -@six.add_metaclass(ABCMeta) -class AbstractOperation(object): - """Base operation class. - - Operation represent a call into database. The call can create, change or - remove data. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - """ - - def __init__(self, project_name, entity_type): - self._project_name = project_name - self._entity_type = entity_type - self._id = str(uuid.uuid4()) - - @property - def project_name(self): - return self._project_name - - @property - def id(self): - """Identifier of operation.""" - - return self._id - - @property - def entity_type(self): - return self._entity_type - - @abstractproperty - def operation_name(self): - """Stringified type of operation.""" - - pass - - @abstractmethod - def to_mongo_operation(self): - """Convert operation to Mongo batch operation.""" - - pass - - def to_data(self): - """Convert operation to data that can be converted to json or others. - - Warning: - Current state returns ObjectId objects which cannot be parsed by - json. - - Returns: - Dict[str, Any]: Description of operation. - """ - - return { - "id": self._id, - "entity_type": self.entity_type, - "project_name": self.project_name, - "operation": self.operation_name - } - - -class CreateOperation(AbstractOperation): - """Operation to create an entity. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - data (Dict[str, Any]): Data of entity that will be created. - """ - - operation_name = "create" - - def __init__(self, project_name, entity_type, data): - super(CreateOperation, self).__init__(project_name, entity_type) - - if not data: - data = {} - else: - data = copy.deepcopy(dict(data)) - - if "_id" not in data: - data["_id"] = ObjectId() - else: - data["_id"] = ObjectId(data["_id"]) - - self._entity_id = data["_id"] - self._data = data - - def __setitem__(self, key, value): - self.set_value(key, value) - - def __getitem__(self, key): - return self.data[key] - - def set_value(self, key, value): - self.data[key] = value - - def get(self, key, *args, **kwargs): - return self.data.get(key, *args, **kwargs) - - @property - def entity_id(self): - return self._entity_id - - @property - def data(self): - return self._data - - def to_mongo_operation(self): - return InsertOne(copy.deepcopy(self._data)) - - def to_data(self): - output = super(CreateOperation, self).to_data() - output["data"] = copy.deepcopy(self.data) - return output - - -class UpdateOperation(AbstractOperation): - """Operation to update an entity. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - entity_id (Union[str, ObjectId]): Identifier of an entity. - update_data (Dict[str, Any]): Key -> value changes that will be set in - database. If value is set to 'REMOVED_VALUE' the key will be - removed. Only first level of dictionary is checked (on purpose). - """ - - operation_name = "update" - - def __init__(self, project_name, entity_type, entity_id, update_data): - super(UpdateOperation, self).__init__(project_name, entity_type) - - self._entity_id = ObjectId(entity_id) - self._update_data = update_data - - @property - def entity_id(self): - return self._entity_id - - @property - def update_data(self): - return self._update_data - - def to_mongo_operation(self): - unset_data = {} - set_data = {} - for key, value in self._update_data.items(): - if value is REMOVED_VALUE: - unset_data[key] = None - else: - set_data[key] = value - - op_data = {} - if unset_data: - op_data["$unset"] = unset_data - if set_data: - op_data["$set"] = set_data - - if not op_data: - return None - - return UpdateOne( - {"_id": self.entity_id}, - op_data - ) - - def to_data(self): - changes = {} - for key, value in self._update_data.items(): - if value is REMOVED_VALUE: - value = None - changes[key] = value - - output = super(UpdateOperation, self).to_data() - output.update({ - "entity_id": self.entity_id, - "changes": changes - }) - return output - - -class DeleteOperation(AbstractOperation): - """Operation to delete an entity. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - entity_id (Union[str, ObjectId]): Entity id that will be removed. - """ - - operation_name = "delete" - - def __init__(self, project_name, entity_type, entity_id): - super(DeleteOperation, self).__init__(project_name, entity_type) - - self._entity_id = ObjectId(entity_id) - - @property - def entity_id(self): - return self._entity_id - - def to_mongo_operation(self): - return DeleteOne({"_id": self.entity_id}) - - def to_data(self): - output = super(DeleteOperation, self).to_data() - output["entity_id"] = self.entity_id - return output - - -class OperationsSession(object): - """Session storing operations that should happen in an order. - - At this moment does not handle anything special can be sonsidered as - stupid list of operations that will happen after each other. If creation - of same entity is there multiple times it's handled in any way and document - values are not validated. - - All operations must be related to single project. - - Args: - project_name (str): Project name to which are operations related. - """ - - def __init__(self): - self._operations = [] - - def add(self, operation): - """Add operation to be processed. - - Args: - operation (BaseOperation): Operation that should be processed. - """ - if not isinstance( - operation, - (CreateOperation, UpdateOperation, DeleteOperation) - ): - raise TypeError("Expected Operation object got {}".format( - str(type(operation)) - )) - - self._operations.append(operation) - - def append(self, operation): - """Add operation to be processed. - - Args: - operation (BaseOperation): Operation that should be processed. - """ - - self.add(operation) - - def extend(self, operations): - """Add operations to be processed. - - Args: - operations (List[BaseOperation]): Operations that should be - processed. - """ - - for operation in operations: - self.add(operation) - - def remove(self, operation): - """Remove operation.""" - - self._operations.remove(operation) - - def clear(self): - """Clear all registered operations.""" - - self._operations = [] - - def to_data(self): - return [ - operation.to_data() - for operation in self._operations - ] - - def commit(self): - """Commit session operations.""" - - operations, self._operations = self._operations, [] - if not operations: - return - - operations_by_project = collections.defaultdict(list) - for operation in operations: - operations_by_project[operation.project_name].append(operation) - - for project_name, operations in operations_by_project.items(): - bulk_writes = [] - for operation in operations: - mongo_op = operation.to_mongo_operation() - if mongo_op is not None: - bulk_writes.append(mongo_op) - - if bulk_writes: - collection = get_project_connection(project_name) - collection.bulk_write(bulk_writes) - - def create_entity(self, project_name, entity_type, data): - """Fast access to 'CreateOperation'. - - Returns: - CreateOperation: Object of update operation. - """ - - operation = CreateOperation(project_name, entity_type, data) - self.add(operation) - return operation - - def update_entity(self, project_name, entity_type, entity_id, update_data): - """Fast access to 'UpdateOperation'. - - Returns: - UpdateOperation: Object of update operation. - """ - - operation = UpdateOperation( - project_name, entity_type, entity_id, update_data - ) - self.add(operation) - return operation - - def delete_entity(self, project_name, entity_type, entity_id): - """Fast access to 'DeleteOperation'. - - Returns: - DeleteOperation: Object of delete operation. - """ - - operation = DeleteOperation(project_name, entity_type, entity_id) - self.add(operation) - return operation - - -def create_project( - project_name, - project_code, - library_project=False, -): - """Create project using OpenPype settings. - - This project creation function is not validating project document on - creation. It is because project document is created blindly with only - minimum required information about project which is it's name, code, type - and schema. - - Entered project name must be unique and project must not exist yet. - - Note: - This function is here to be OP v4 ready but in v3 has more logic - to do. That's why inner imports are in the body. - - Args: - project_name(str): New project name. Should be unique. - project_code(str): Project's code should be unique too. - library_project(bool): Project is library project. - - Raises: - ValueError: When project name already exists in MongoDB. - - Returns: - dict: Created project document. - """ - - from openpype.settings import ProjectSettings, SaveWarningExc - from openpype.pipeline.schema import validate - - if get_project(project_name, fields=["name"]): - raise ValueError("Project with name \"{}\" already exists".format( - project_name - )) - - if not PROJECT_NAME_REGEX.match(project_name): - raise ValueError(( - "Project name \"{}\" contain invalid characters" - ).format(project_name)) - - project_doc = { - "type": "project", - "name": project_name, - "data": { - "code": project_code, - "library_project": library_project, - }, - "schema": CURRENT_PROJECT_SCHEMA - } - - op_session = OperationsSession() - # Insert document with basic data - create_op = op_session.create_entity( - project_name, project_doc["type"], project_doc +else: + from ayon_api.server_api import ( + PROJECT_NAME_ALLOWED_SYMBOLS, + PROJECT_NAME_REGEX, + ) + from .server.operations import * + from .mongo.operations import ( + CURRENT_PROJECT_SCHEMA, + CURRENT_PROJECT_CONFIG_SCHEMA, + CURRENT_ASSET_DOC_SCHEMA, + CURRENT_SUBSET_SCHEMA, + CURRENT_VERSION_SCHEMA, + CURRENT_HERO_VERSION_SCHEMA, + CURRENT_REPRESENTATION_SCHEMA, + CURRENT_WORKFILE_INFO_SCHEMA, + CURRENT_THUMBNAIL_SCHEMA ) - op_session.commit() - - # Load ProjectSettings for the project and save it to store all attributes - # and Anatomy - try: - project_settings_entity = ProjectSettings(project_name) - project_settings_entity.save() - except SaveWarningExc as exc: - print(str(exc)) - except Exception: - op_session.delete_entity( - project_name, project_doc["type"], create_op.entity_id - ) - op_session.commit() - raise - - project_doc = get_project(project_name) - - try: - # Validate created project document - validate(project_doc) - except Exception: - # Remove project if is not valid - op_session.delete_entity( - project_name, project_doc["type"], create_op.entity_id - ) - op_session.commit() - raise - - return project_doc diff --git a/openpype/client/operations_base.py b/openpype/client/operations_base.py new file mode 100644 index 0000000000..887b237b1c --- /dev/null +++ b/openpype/client/operations_base.py @@ -0,0 +1,289 @@ +import uuid +import copy +from abc import ABCMeta, abstractmethod, abstractproperty +import six + +REMOVED_VALUE = object() + + +@six.add_metaclass(ABCMeta) +class AbstractOperation(object): + """Base operation class. + + Operation represent a call into database. The call can create, change or + remove data. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + """ + + def __init__(self, project_name, entity_type): + self._project_name = project_name + self._entity_type = entity_type + self._id = str(uuid.uuid4()) + + @property + def project_name(self): + return self._project_name + + @property + def id(self): + """Identifier of operation.""" + + return self._id + + @property + def entity_type(self): + return self._entity_type + + @abstractproperty + def operation_name(self): + """Stringified type of operation.""" + + pass + + def to_data(self): + """Convert operation to data that can be converted to json or others. + + Warning: + Current state returns ObjectId objects which cannot be parsed by + json. + + Returns: + Dict[str, Any]: Description of operation. + """ + + return { + "id": self._id, + "entity_type": self.entity_type, + "project_name": self.project_name, + "operation": self.operation_name + } + + +class CreateOperation(AbstractOperation): + """Operation to create an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + data (Dict[str, Any]): Data of entity that will be created. + """ + + operation_name = "create" + + def __init__(self, project_name, entity_type, data): + super(CreateOperation, self).__init__(project_name, entity_type) + + if not data: + data = {} + else: + data = copy.deepcopy(dict(data)) + self._data = data + + def __setitem__(self, key, value): + self.set_value(key, value) + + def __getitem__(self, key): + return self.data[key] + + def set_value(self, key, value): + self.data[key] = value + + def get(self, key, *args, **kwargs): + return self.data.get(key, *args, **kwargs) + + @abstractproperty + def entity_id(self): + pass + + @property + def data(self): + return self._data + + def to_data(self): + output = super(CreateOperation, self).to_data() + output["data"] = copy.deepcopy(self.data) + return output + + +class UpdateOperation(AbstractOperation): + """Operation to update an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + entity_id (Union[str, ObjectId]): Identifier of an entity. + update_data (Dict[str, Any]): Key -> value changes that will be set in + database. If value is set to 'REMOVED_VALUE' the key will be + removed. Only first level of dictionary is checked (on purpose). + """ + + operation_name = "update" + + def __init__(self, project_name, entity_type, entity_id, update_data): + super(UpdateOperation, self).__init__(project_name, entity_type) + + self._entity_id = entity_id + self._update_data = update_data + + @property + def entity_id(self): + return self._entity_id + + @property + def update_data(self): + return self._update_data + + def to_data(self): + changes = {} + for key, value in self._update_data.items(): + if value is REMOVED_VALUE: + value = None + changes[key] = value + + output = super(UpdateOperation, self).to_data() + output.update({ + "entity_id": self.entity_id, + "changes": changes + }) + return output + + +class DeleteOperation(AbstractOperation): + """Operation to delete an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + entity_id (Union[str, ObjectId]): Entity id that will be removed. + """ + + operation_name = "delete" + + def __init__(self, project_name, entity_type, entity_id): + super(DeleteOperation, self).__init__(project_name, entity_type) + + self._entity_id = entity_id + + @property + def entity_id(self): + return self._entity_id + + def to_data(self): + output = super(DeleteOperation, self).to_data() + output["entity_id"] = self.entity_id + return output + + +class BaseOperationsSession(object): + """Session storing operations that should happen in an order. + + At this moment does not handle anything special can be considered as + stupid list of operations that will happen after each other. If creation + of same entity is there multiple times it's handled in any way and document + values are not validated. + """ + + def __init__(self): + self._operations = [] + + def __len__(self): + return len(self._operations) + + def add(self, operation): + """Add operation to be processed. + + Args: + operation (BaseOperation): Operation that should be processed. + """ + if not isinstance( + operation, + (CreateOperation, UpdateOperation, DeleteOperation) + ): + raise TypeError("Expected Operation object got {}".format( + str(type(operation)) + )) + + self._operations.append(operation) + + def append(self, operation): + """Add operation to be processed. + + Args: + operation (BaseOperation): Operation that should be processed. + """ + + self.add(operation) + + def extend(self, operations): + """Add operations to be processed. + + Args: + operations (List[BaseOperation]): Operations that should be + processed. + """ + + for operation in operations: + self.add(operation) + + def remove(self, operation): + """Remove operation.""" + + self._operations.remove(operation) + + def clear(self): + """Clear all registered operations.""" + + self._operations = [] + + def to_data(self): + return [ + operation.to_data() + for operation in self._operations + ] + + @abstractmethod + def commit(self): + """Commit session operations.""" + pass + + def create_entity(self, project_name, entity_type, data): + """Fast access to 'CreateOperation'. + + Returns: + CreateOperation: Object of update operation. + """ + + operation = CreateOperation(project_name, entity_type, data) + self.add(operation) + return operation + + def update_entity(self, project_name, entity_type, entity_id, update_data): + """Fast access to 'UpdateOperation'. + + Returns: + UpdateOperation: Object of update operation. + """ + + operation = UpdateOperation( + project_name, entity_type, entity_id, update_data + ) + self.add(operation) + return operation + + def delete_entity(self, project_name, entity_type, entity_id): + """Fast access to 'DeleteOperation'. + + Returns: + DeleteOperation: Object of delete operation. + """ + + operation = DeleteOperation(project_name, entity_type, entity_id) + self.add(operation) + return operation diff --git a/openpype/client/server/__init__.py b/openpype/client/server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/client/server/constants.py b/openpype/client/server/constants.py new file mode 100644 index 0000000000..7ff990dc60 --- /dev/null +++ b/openpype/client/server/constants.py @@ -0,0 +1,83 @@ +# --- Project --- +DEFAULT_PROJECT_FIELDS = { + "active", + "name", + "code", + "config", + "data", + "createdAt", +} + +# --- Folders --- +DEFAULT_FOLDER_FIELDS = { + "id", + "name", + "path", + "parentId", + "active", + "parents", + "thumbnailId" +} + +# --- Tasks --- +DEFAULT_TASK_FIELDS = { + "id", + "name", + "taskType", + "assignees", +} + +# --- Subsets --- +DEFAULT_SUBSET_FIELDS = { + "id", + "name", + "active", + "family", + "folderId", +} + +# --- Versions --- +DEFAULT_VERSION_FIELDS = { + "id", + "name", + "version", + "active", + "subsetId", + "taskId", + "author", + "thumbnailId", + "createdAt", + "updatedAt", +} + +# --- Representations --- +DEFAULT_REPRESENTATION_FIELDS = { + "id", + "name", + "context", + "createdAt", + "active", + "versionId", +} + +REPRESENTATION_FILES_FIELDS = { + "files.name", + "files.hash", + "files.id", + "files.path", + "files.size", +} + +DEFAULT_WORKFILE_INFO_FIELDS = { + "active", + "createdAt", + "createdBy", + "id", + "name", + "path", + "projectName", + "taskId", + "thumbnailId", + "updatedAt", + "updatedBy", +} diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py new file mode 100644 index 0000000000..e8c2ee9c3c --- /dev/null +++ b/openpype/client/server/conversion_utils.py @@ -0,0 +1,1244 @@ +import os +import datetime +import collections +import json + +import six + +from openpype.client.operations_base import REMOVED_VALUE +from openpype.client.mongo.operations import ( + CURRENT_PROJECT_SCHEMA, + CURRENT_ASSET_DOC_SCHEMA, + CURRENT_SUBSET_SCHEMA, + CURRENT_VERSION_SCHEMA, + CURRENT_HERO_VERSION_SCHEMA, + CURRENT_REPRESENTATION_SCHEMA, + CURRENT_WORKFILE_INFO_SCHEMA, +) +from .constants import REPRESENTATION_FILES_FIELDS +from .utils import create_entity_id, prepare_entity_changes + +# --- Project entity --- +PROJECT_FIELDS_MAPPING_V3_V4 = { + "_id": {"name"}, + "name": {"name"}, + "data": {"data", "code"}, + "data.library_project": {"library"}, + "data.code": {"code"}, + "data.active": {"active"}, +} + +# TODO this should not be hardcoded but received from server!!! +# --- Folder entity --- +FOLDER_FIELDS_MAPPING_V3_V4 = { + "_id": {"id"}, + "name": {"name"}, + "label": {"label"}, + "data": { + "parentId", "parents", "active", "tasks", "thumbnailId" + }, + "data.visualParent": {"parentId"}, + "data.parents": {"parents"}, + "data.active": {"active"}, + "data.thumbnail_id": {"thumbnailId"}, + "data.entityType": {"folderType"} +} + +# --- Subset entity --- +SUBSET_FIELDS_MAPPING_V3_V4 = { + "_id": {"id"}, + "name": {"name"}, + "data.active": {"active"}, + "parent": {"folderId"} +} + +# --- Version entity --- +VERSION_FIELDS_MAPPING_V3_V4 = { + "_id": {"id"}, + "name": {"version"}, + "parent": {"subsetId"} +} + +# --- Representation entity --- +REPRESENTATION_FIELDS_MAPPING_V3_V4 = { + "_id": {"id"}, + "name": {"name"}, + "parent": {"versionId"}, + "context": {"context"}, + "files": {"files"}, +} + + +def project_fields_v3_to_v4(fields, con): + """Convert project fields from v3 to v4 structure. + + Args: + fields (Union[Iterable(str), None]): fields to be converted. + + Returns: + Union[Set(str), None]: Converted fields to v4 fields. + """ + + # TODO config fields + # - config.apps + # - config.groups + if not fields: + return None + + project_attribs = con.get_attributes_for_type("project") + output = set() + for field in fields: + # If config is needed the rest api call must be used + if field.startswith("config"): + return None + + if field in PROJECT_FIELDS_MAPPING_V3_V4: + output |= PROJECT_FIELDS_MAPPING_V3_V4[field] + if field == "data": + output |= { + "attrib.{}".format(attr) + for attr in project_attribs + } + + elif field.startswith("data"): + field_parts = field.split(".") + field_parts.pop(0) + data_key = ".".join(field_parts) + if data_key in project_attribs: + output.add("attrib.{}".format(data_key)) + else: + output.add("data") + print("Requested specific key from data {}".format(data_key)) + + else: + raise ValueError("Unknown field mapping for {}".format(field)) + + if "name" not in output: + output.add("name") + return output + + +def _get_default_template_name(templates): + default_template = None + for name, template in templates.items(): + if name == "default": + return "default" + + if default_template is None: + default_template = template["name"] + + return default_template + + +def _convert_template_item(template): + template["folder"] = template.pop("directory") + template["path"] = "/".join( + (template["folder"], template["file"]) + ) + + +def _fill_template_category(templates, cat_templates, cat_key): + default_template_name = _get_default_template_name(cat_templates) + for template_name, cat_template in cat_templates.items(): + _convert_template_item(cat_template) + if template_name == default_template_name: + templates[cat_key] = cat_template + else: + new_name = "{}_{}".format(cat_key, template_name) + templates["others"][new_name] = cat_template + + +def convert_v4_project_to_v3(project): + """Convert Project entity data from v4 structure to v3 structure. + + Args: + project (Dict[str, Any]): Project entity queried from v4 server. + + Returns: + Dict[str, Any]: Project converted to v3 structure. + """ + + if not project: + return project + + project_name = project["name"] + output = { + "_id": project_name, + "name": project_name, + "schema": CURRENT_PROJECT_SCHEMA, + "type": "project" + } + + data = project.get("data") or {} + attribs = project.get("attrib") or {} + apps_attr = attribs.pop("applications", None) or [] + applications = [ + {"name": app_name} + for app_name in apps_attr + ] + data.update(attribs) + data["entityType"] = "Project" + + config = {} + project_config = project.get("config") + + if project_config: + config["apps"] = applications + config["roots"] = project_config["roots"] + + templates = project_config["templates"] + templates["defaults"] = templates.pop("common", None) or {} + + others_templates = templates.pop("others", None) or {} + new_others_templates = {} + templates["others"] = new_others_templates + for name, template in others_templates.items(): + _convert_template_item(template) + new_others_templates[name] = template + + for key in ( + "work", + "publish", + "hero" + ): + cat_templates = templates.pop(key) + _fill_template_category(templates, cat_templates, key) + + delivery_templates = templates.pop("delivery", None) or {} + new_delivery_templates = {} + for name, delivery_template in delivery_templates.items(): + new_delivery_templates[name] = "/".join( + (delivery_template["directory"], delivery_template["file"]) + ) + templates["delivery"] = new_delivery_templates + + config["templates"] = templates + + if "taskTypes" in project: + task_types = project["taskTypes"] + new_task_types = {} + for task_type in task_types: + name = task_type.pop("name") + new_task_types[name] = task_type + + config["tasks"] = new_task_types + + if config: + output["config"] = config + + for data_key, key in ( + ("library_project", "library"), + ("code", "code"), + ("active", "active") + ): + if key in project: + data[data_key] = project[key] + + if "attrib" in project: + for key, value in project["attrib"].items(): + data[key] = value + + if data: + output["data"] = data + return output + + +def folder_fields_v3_to_v4(fields, con): + """Convert folder fields from v3 to v4 structure. + + Args: + fields (Union[Iterable(str), None]): fields to be converted. + + Returns: + Union[Set(str), None]: Converted fields to v4 fields. + """ + + if not fields: + return None + + folder_attributes = con.get_attributes_for_type("folder") + output = set() + for field in fields: + if field in ("schema", "type", "parent"): + continue + + if field in FOLDER_FIELDS_MAPPING_V3_V4: + output |= FOLDER_FIELDS_MAPPING_V3_V4[field] + if field == "data": + output |= { + "attrib.{}".format(attr) + for attr in folder_attributes + } + + elif field.startswith("data"): + field_parts = field.split(".") + field_parts.pop(0) + data_key = ".".join(field_parts) + if data_key == "label": + output.add("name") + + elif data_key in ("icon", "color"): + continue + + elif data_key.startswith("tasks"): + output.add("tasks") + + elif data_key in folder_attributes: + output.add("attrib.{}".format(data_key)) + + else: + output.add("data") + print("Requested specific key from data {}".format(data_key)) + + else: + raise ValueError("Unknown field mapping for {}".format(field)) + + if "id" not in output: + output.add("id") + return output + + +def convert_v4_tasks_to_v3(tasks): + """Convert v4 task item to v3 task. + + Args: + tasks (List[Dict[str, Any]]): Task entites. + + Returns: + Dict[str, Dict[str, Any]]: Tasks in v3 variant ready for v3 asset. + """ + + output = {} + for task in tasks: + task_name = task["name"] + new_task = { + "type": task["taskType"] + } + output[task_name] = new_task + return output + + +def convert_v4_folder_to_v3(folder, project_name): + """Convert v4 folder to v3 asset. + + Args: + folder (Dict[str, Any]): Folder entity data. + project_name (str): Project name from which folder was queried. + + Returns: + Dict[str, Any]: Converted v4 folder to v3 asset. + """ + + output = { + "_id": folder["id"], + "parent": project_name, + "type": "asset", + "schema": CURRENT_ASSET_DOC_SCHEMA + } + + output_data = folder.get("data") or {} + + if "name" in folder: + output["name"] = folder["name"] + output_data["label"] = folder["name"] + + if "folderType" in folder: + output_data["entityType"] = folder["folderType"] + + for src_key, dst_key in ( + ("parentId", "visualParent"), + ("active", "active"), + ("thumbnailId", "thumbnail_id"), + ("parents", "parents"), + ): + if src_key in folder: + output_data[dst_key] = folder[src_key] + + if "attrib" in folder: + output_data.update(folder["attrib"]) + + if "tasks" in folder: + output_data["tasks"] = convert_v4_tasks_to_v3(folder["tasks"]) + + output["data"] = output_data + + return output + + +def subset_fields_v3_to_v4(fields, con): + """Convert subset fields from v3 to v4 structure. + + Args: + fields (Union[Iterable(str), None]): fields to be converted. + + Returns: + Union[Set(str), None]: Converted fields to v4 fields. + """ + + if not fields: + return None + + subset_attributes = con.get_attributes_for_type("subset") + + output = set() + for field in fields: + if field in ("schema", "type"): + continue + + if field in SUBSET_FIELDS_MAPPING_V3_V4: + output |= SUBSET_FIELDS_MAPPING_V3_V4[field] + + elif field == "data": + output.add("family") + output.add("active") + output |= { + "attrib.{}".format(attr) + for attr in subset_attributes + } + + elif field.startswith("data"): + field_parts = field.split(".") + field_parts.pop(0) + data_key = ".".join(field_parts) + if data_key in ("family", "families"): + output.add("family") + + elif data_key in subset_attributes: + output.add("attrib.{}".format(data_key)) + + else: + output.add("data") + print("Requested specific key from data {}".format(data_key)) + + else: + raise ValueError("Unknown field mapping for {}".format(field)) + + if "id" not in output: + output.add("id") + return output + + +def convert_v4_subset_to_v3(subset): + output = { + "_id": subset["id"], + "type": "subset", + "schema": CURRENT_SUBSET_SCHEMA + } + if "folderId" in subset: + output["parent"] = subset["folderId"] + + output_data = subset.get("data") or {} + + if "name" in subset: + output["name"] = subset["name"] + + if "active" in subset: + output_data["active"] = subset["active"] + + if "attrib" in subset: + attrib = subset["attrib"] + output_data.update(attrib) + + family = subset.get("family") + if family: + output_data["family"] = family + output_data["families"] = [family] + + output["data"] = output_data + + return output + + +def version_fields_v3_to_v4(fields, con): + """Convert version fields from v3 to v4 structure. + + Args: + fields (Union[Iterable(str), None]): fields to be converted. + + Returns: + Union[Set(str), None]: Converted fields to v4 fields. + """ + + if not fields: + return None + + version_attributes = con.get_attributes_for_type("version") + + output = set() + for field in fields: + if field in ("type", "schema", "version_id"): + continue + + if field in VERSION_FIELDS_MAPPING_V3_V4: + output |= VERSION_FIELDS_MAPPING_V3_V4[field] + + elif field == "data": + output |= { + "attrib.{}".format(attr) + for attr in version_attributes + } + output |= { + "author", + "createdAt", + "thumbnailId", + } + + elif field.startswith("data"): + field_parts = field.split(".") + field_parts.pop(0) + data_key = ".".join(field_parts) + if data_key in version_attributes: + output.add("attrib.{}".format(data_key)) + + elif data_key == "thumbnail_id": + output.add("thumbnailId") + + elif data_key == "time": + output.add("createdAt") + + elif data_key == "author": + output.add("author") + + elif data_key in ("tags", ): + continue + + else: + output.add("data") + print("Requested specific key from data {}".format(data_key)) + + else: + raise ValueError("Unknown field mapping for {}".format(field)) + + if "id" not in output: + output.add("id") + return output + + +def convert_v4_version_to_v3(version): + """Convert v4 version entity to v4 version. + + Args: + version (Dict[str, Any]): Queried v4 version entity. + + Returns: + Dict[str, Any]: Conveted version entity to v3 structure. + """ + + version_num = version["version"] + if version_num < 0: + output = { + "_id": version["id"], + "type": "hero_version", + "schema": CURRENT_HERO_VERSION_SCHEMA, + } + if "subsetId" in version: + output["parent"] = version["subsetId"] + + if "data" in version: + output["data"] = version["data"] + return output + + output = { + "_id": version["id"], + "type": "version", + "name": version_num, + "schema": CURRENT_VERSION_SCHEMA + } + if "subsetId" in version: + output["parent"] = version["subsetId"] + + output_data = version.get("data") or {} + if "attrib" in version: + output_data.update(version["attrib"]) + + for src_key, dst_key in ( + ("active", "active"), + ("thumbnailId", "thumbnail_id"), + ("author", "author") + ): + if src_key in version: + output_data[dst_key] = version[src_key] + + if "createdAt" in version: + created_at = datetime.datetime.fromisoformat(version["createdAt"]) + output_data["time"] = created_at.strftime("%Y%m%dT%H%M%SZ") + + output["data"] = output_data + + return output + + +def representation_fields_v3_to_v4(fields, con): + """Convert representation fields from v3 to v4 structure. + + Args: + fields (Union[Iterable(str), None]): fields to be converted. + + Returns: + Union[Set(str), None]: Converted fields to v4 fields. + """ + + if not fields: + return None + + representation_attributes = con.get_attributes_for_type("representation") + + output = set() + for field in fields: + if field in ("type", "schema"): + continue + + if field in REPRESENTATION_FIELDS_MAPPING_V3_V4: + output |= REPRESENTATION_FIELDS_MAPPING_V3_V4[field] + + elif field.startswith("context"): + output.add("context") + + # TODO: 'files' can have specific attributes but the keys in v3 and v4 + # are not the same (content is not the same) + elif field.startswith("files"): + output |= REPRESENTATION_FILES_FIELDS + + elif field.startswith("data"): + fields |= { + "attrib.{}".format(attr) + for attr in representation_attributes + } + + else: + raise ValueError("Unknown field mapping for {}".format(field)) + + if "id" not in output: + output.add("id") + return output + + +def convert_v4_representation_to_v3(representation): + """Convert v4 representation to v3 representation. + + Args: + representation (Dict[str, Any]): Queried representation from v4 server. + + Returns: + Dict[str, Any]: Converted representation to v3 structure. + """ + + output = { + "type": "representation", + "schema": CURRENT_REPRESENTATION_SCHEMA, + } + if "id" in representation: + output["_id"] = representation["id"] + + for v3_key, v4_key in ( + ("name", "name"), + ("parent", "versionId") + ): + if v4_key in representation: + output[v3_key] = representation[v4_key] + + if "context" in representation: + context = representation["context"] + if isinstance(context, six.string_types): + context = json.loads(context) + output["context"] = context + + if "files" in representation: + files = representation["files"] + new_files = [] + # From GraphQl is list + if isinstance(files, list): + for file_info in files: + file_info["_id"] = file_info["id"] + new_files.append(file_info) + + # From RestPoint is dictionary + elif isinstance(files, dict): + for file_id, file_info in files: + file_info["_id"] = file_id + new_files.append(file_info) + + if not new_files: + new_files.append({ + "name": "studio" + }) + output["files"] = new_files + + if representation.get("active") is False: + output["type"] = "archived_representation" + output["old_id"] = output["_id"] + + output_data = representation.get("data") or {} + if "attrib" in representation: + output_data.update(representation["attrib"]) + + for key, data_key in ( + ("active", "active"), + ): + if key in representation: + output_data[data_key] = representation[key] + + output["data"] = output_data + + return output + + +def workfile_info_fields_v3_to_v4(fields): + if not fields: + return None + + new_fields = set() + fields = set(fields) + for v3_key, v4_key in ( + ("_id", "id"), + ("files", "path"), + ("filename", "name"), + ("data", "data"), + ): + if v3_key in fields: + new_fields.add(v4_key) + + if "parent" in fields or "task_name" in fields: + new_fields.add("taskId") + + return new_fields + + +def convert_v4_workfile_info_to_v3(workfile_info, task): + output = { + "type": "representation", + "schema": CURRENT_WORKFILE_INFO_SCHEMA, + } + if "id" in workfile_info: + output["_id"] = workfile_info["id"] + + if "path" in workfile_info: + output["files"] = [workfile_info["path"]] + + if "name" in workfile_info: + output["filename"] = workfile_info["name"] + + if "taskId" in workfile_info: + output["task_name"] = task["name"] + output["parent"] = task["folderId"] + + return output + + +def convert_create_asset_to_v4(asset, project, con): + folder_attributes = con.get_attributes_for_type("folder") + + asset_data = asset["data"] + parent_id = asset_data["visualParent"] + + folder = { + "name": asset["name"], + "parentId": parent_id, + } + entity_id = asset.get("_id") + if entity_id: + folder["id"] = entity_id + + attribs = {} + data = {} + for key, value in asset_data.items(): + if key in ( + "visualParent", + "thumbnail_id", + "parents", + "inputLinks", + "avalon_mongo_id", + ): + continue + + if key not in folder_attributes: + data[key] = value + elif value is not None: + attribs[key] = value + + if attribs: + folder["attrib"] = attribs + + if data: + folder["data"] = data + return folder + + +def convert_create_task_to_v4(task, project, con): + if not project["taskTypes"]: + raise ValueError( + "Project \"{}\" does not have any task types".format( + project["name"])) + + task_type = task["type"] + if task_type not in project["taskTypes"]: + task_type = tuple(project["taskTypes"].keys())[0] + + return { + "name": task["name"], + "taskType": task_type, + "folderId": task["folderId"] + } + + +def convert_create_subset_to_v4(subset, con): + subset_attributes = con.get_attributes_for_type("subset") + + subset_data = subset["data"] + family = subset_data.get("family") + if not family: + family = subset_data["families"][0] + + converted_subset = { + "name": subset["name"], + "family": family, + "folderId": subset["parent"], + } + entity_id = subset.get("_id") + if entity_id: + converted_subset["id"] = entity_id + + attribs = {} + data = {} + for key, value in subset_data.items(): + if key not in subset_attributes: + data[key] = value + elif value is not None: + attribs[key] = value + + if attribs: + converted_subset["attrib"] = attribs + + if data: + converted_subset["data"] = data + + return converted_subset + + +def convert_create_version_to_v4(version, con): + version_attributes = con.get_attributes_for_type("version") + converted_version = { + "version": version["name"], + "subsetId": version["parent"], + } + entity_id = version.get("_id") + if entity_id: + converted_version["id"] = entity_id + + version_data = version["data"] + attribs = {} + data = {} + for key, value in version_data.items(): + if key not in version_attributes: + data[key] = value + elif value is not None: + attribs[key] = value + + if attribs: + converted_version["attrib"] = attribs + + if data: + converted_version["data"] = attribs + + return converted_version + + +def convert_create_hero_version_to_v4(hero_version, project_name, con): + if "version_id" in hero_version: + version_id = hero_version["version_id"] + version = con.get_version_by_id(project_name, version_id) + version["version"] = - version["version"] + + for auto_key in ( + "name", + "createdAt", + "updatedAt", + "author", + ): + version.pop(auto_key, None) + + return version + + version_attributes = con.get_attributes_for_type("version") + converted_version = { + "version": hero_version["version"], + "subsetId": hero_version["parent"], + } + entity_id = hero_version.get("_id") + if entity_id: + converted_version["id"] = entity_id + + version_data = hero_version["data"] + attribs = {} + data = {} + for key, value in version_data.items(): + if key not in version_attributes: + data[key] = value + elif value is not None: + attribs[key] = value + + if attribs: + converted_version["attrib"] = attribs + + if data: + converted_version["data"] = attribs + + return converted_version + + +def convert_create_representation_to_v4(representation, con): + representation_attributes = con.get_attributes_for_type("representation") + + converted_representation = { + "name": representation["name"], + "versionId": representation["parent"], + } + entity_id = representation.get("_id") + if entity_id: + converted_representation["id"] = entity_id + + if representation.get("type") == "archived_representation": + converted_representation["active"] = False + + new_files = {} + for file_item in representation["files"]: + new_file_item = { + key: value + for key, value in file_item.items() + if key != "_id" + } + file_item_id = create_entity_id() + new_files[file_item_id] = new_file_item + + attribs = {} + data = { + "files": new_files, + "context": representation["context"] + } + + representation_data = representation["data"] + + for key, value in representation_data.items(): + if key not in representation_attributes: + data[key] = value + elif value is not None: + attribs[key] = value + + if attribs: + converted_representation["attrib"] = attribs + + if data: + converted_representation["data"] = data + + return converted_representation + + +def convert_create_workfile_info_to_v4(data, project_name, con): + folder_id = data["parent"] + task_name = data["task_name"] + task = con.get_task_by_name(project_name, folder_id, task_name) + if not task: + return None + + workfile_attributes = con.get_attributes_for_type("workfile") + filename = data["filename"] + possible_attribs = { + "extension": os.path.splitext(filename)[-1] + } + attribs = {} + for attr in workfile_attributes: + if attr in possible_attribs: + attribs[attr] = possible_attribs[attr] + + output = { + "path": data["files"][0], + "name": filename, + "taskId": task["id"] + } + if "_id" in data: + output["id"] = data["_id"] + + if attribs: + output["attrib"] = attribs + + output_data = data.get("data") + if output_data: + output["data"] = output_data + return output + + +def _from_flat_dict(data): + output = {} + for key, value in data.items(): + output_value = output + subkeys = key.split(".") + last_key = subkeys.pop(-1) + for subkey in subkeys: + if subkey not in output_value: + output_value[subkey] = {} + output_value = output_value[subkey] + + output_value[last_key] = value + return output + + +def _to_flat_dict(data): + output = {} + flat_queue = collections.deque() + flat_queue.append(([], data)) + while flat_queue: + item = flat_queue.popleft() + parent_keys, data = item + for key, value in data.items(): + keys = list(parent_keys) + keys.append(key) + if isinstance(value, dict): + flat_queue.append((keys, value)) + else: + full_key = ".".join(keys) + output[full_key] = value + + return output + + +def convert_update_folder_to_v4(project_name, asset_id, update_data, con): + new_update_data = {} + + folder_attributes = con.get_attributes_for_type("folder") + full_update_data = _from_flat_dict(update_data) + data = full_update_data.get("data") + + has_new_parent = False + has_task_changes = False + parent_id = None + tasks = None + new_data = {} + attribs = {} + if "type" in update_data: + new_update_data["active"] = update_data["type"] == "asset" + + if data: + if "thumbnail_id" in data: + new_update_data["thumbnailId"] = data.pop("thumbnail_id") + + if "tasks" in data: + tasks = data.pop("tasks") + has_task_changes = True + + if "visualParent" in data: + has_new_parent = True + parent_id = data.pop("visualParent") + + for key, value in data.items(): + if key in folder_attributes: + attribs[key] = value + else: + new_data[key] = value + + if "name" in update_data: + new_update_data["name"] = update_data["name"] + + if "type" in update_data: + new_type = update_data["type"] + if new_type == "asset": + new_update_data["active"] = True + elif new_type == "archived_asset": + new_update_data["active"] = False + + if has_new_parent: + new_update_data["parentId"] = parent_id + + if new_data: + print("Folder has new data: {}".format(new_data)) + new_update_data["data"] = new_data + + if has_task_changes: + raise ValueError("Task changes of folder are not implemented") + + return _to_flat_dict(new_update_data) + + +def convert_update_subset_to_v4(project_name, subset_id, update_data, con): + new_update_data = {} + + subset_attributes = con.get_attributes_for_type("subset") + full_update_data = _from_flat_dict(update_data) + data = full_update_data.get("data") + new_data = {} + attribs = {} + if data: + if "family" in data: + family = data.pop("family") + new_update_data["family"] = family + + if "families" in data: + families = data.pop("families") + if "family" not in new_update_data: + new_update_data["family"] = families[0] + + for key, value in data.items(): + if key in subset_attributes: + if value is REMOVED_VALUE: + value = None + attribs[key] = value + + elif value is not REMOVED_VALUE: + new_data[key] = value + + if attribs: + new_update_data["attribs"] = attribs + + if "name" in update_data: + new_update_data["name"] = update_data["name"] + + if "type" in update_data: + new_type = update_data["type"] + if new_type == "subset": + new_update_data["active"] = True + elif new_type == "archived_subset": + new_update_data["active"] = False + + if "parent" in update_data: + new_update_data["folderId"] = update_data["parent"] + + flat_data = _to_flat_dict(new_update_data) + if new_data: + print("Subset has new data: {}".format(new_data)) + flat_data["data"] = new_data + + return flat_data + + +def convert_update_version_to_v4(project_name, version_id, update_data, con): + new_update_data = {} + + version_attributes = con.get_attributes_for_type("version") + full_update_data = _from_flat_dict(update_data) + data = full_update_data.get("data") + new_data = {} + attribs = {} + if data: + if "author" in data: + new_update_data["author"] = data.pop("author") + + if "thumbnail_id" in data: + new_update_data["thumbnailId"] = data.pop("thumbnail_id") + + for key, value in data.items(): + if key in version_attributes: + if value is REMOVED_VALUE: + value = None + attribs[key] = value + + elif value is not REMOVED_VALUE: + new_data[key] = value + + if attribs: + new_update_data["attribs"] = attribs + + if "name" in update_data: + new_update_data["version"] = update_data["name"] + + if "type" in update_data: + new_type = update_data["type"] + if new_type == "version": + new_update_data["active"] = True + elif new_type == "archived_version": + new_update_data["active"] = False + + if "parent" in update_data: + new_update_data["subsetId"] = update_data["parent"] + + flat_data = _to_flat_dict(new_update_data) + if new_data: + print("Version has new data: {}".format(new_data)) + flat_data["data"] = new_data + return flat_data + + +def convert_update_hero_version_to_v4( + project_name, hero_version_id, update_data, con +): + if "version_id" not in update_data: + return None + + version_id = update_data["version_id"] + hero_version = con.get_hero_version_by_id(project_name, hero_version_id) + version = con.get_version_by_id(project_name, version_id) + version["version"] = - version["version"] + version["id"] = hero_version_id + + for auto_key in ( + "name", + "createdAt", + "updatedAt", + "author", + ): + version.pop(auto_key, None) + + return prepare_entity_changes(hero_version, version) + + +def convert_update_representation_to_v4( + project_name, repre_id, update_data, con +): + new_update_data = {} + + folder_attributes = con.get_attributes_for_type("folder") + full_update_data = _from_flat_dict(update_data) + data = full_update_data.get("data") + + new_data = {} + attribs = {} + if data: + for key, value in data.items(): + if key in folder_attributes: + attribs[key] = value + else: + new_data[key] = value + + if "name" in update_data: + new_update_data["name"] = update_data["name"] + + if "type" in update_data: + new_type = update_data["type"] + if new_type == "representation": + new_update_data["active"] = True + elif new_type == "archived_representation": + new_update_data["active"] = False + + if "parent" in update_data: + new_update_data["versionId"] = update_data["parent"] + + if "context" in update_data: + new_data["context"] = update_data["context"] + + if "files" in update_data: + new_files = update_data["files"] + if isinstance(new_files, list): + _new_files = {} + for file_item in new_files: + _file_item = { + key: value + for key, value in file_item.items() + if key != "_id" + } + file_item_id = create_entity_id() + _new_files[file_item_id] = _file_item + new_files = _new_files + new_data["files"] = new_files + + flat_data = _to_flat_dict(new_update_data) + if new_data: + print("Representation has new data: {}".format(new_data)) + flat_data["data"] = new_data + + return flat_data + + +def convert_update_workfile_info_to_v4(update_data): + return { + key: value + for key, value in update_data.items() + if key.startswith("data") + } diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py new file mode 100644 index 0000000000..5dc8af9a6d --- /dev/null +++ b/openpype/client/server/entities.py @@ -0,0 +1,655 @@ +import collections + +from ayon_api import get_server_api_connection + +from openpype.client.mongo.operations import CURRENT_THUMBNAIL_SCHEMA + +from .openpype_comp import get_folders_with_tasks +from .conversion_utils import ( + project_fields_v3_to_v4, + convert_v4_project_to_v3, + + folder_fields_v3_to_v4, + convert_v4_folder_to_v3, + + subset_fields_v3_to_v4, + convert_v4_subset_to_v3, + + version_fields_v3_to_v4, + convert_v4_version_to_v3, + + representation_fields_v3_to_v4, + convert_v4_representation_to_v3, + + workfile_info_fields_v3_to_v4, + convert_v4_workfile_info_to_v3, +) + + +def get_projects(active=True, inactive=False, library=None, fields=None): + if not active and not inactive: + return + + if active and inactive: + active = None + elif active: + active = True + elif inactive: + active = False + + con = get_server_api_connection() + fields = project_fields_v3_to_v4(fields, con) + for project in con.get_projects(active, library, fields=fields): + yield convert_v4_project_to_v3(project) + + +def get_project(project_name, active=True, inactive=False, fields=None): + # Skip if both are disabled + con = get_server_api_connection() + fields = project_fields_v3_to_v4(fields, con) + return convert_v4_project_to_v3( + con.get_project(project_name, fields=fields) + ) + + +def get_whole_project(*args, **kwargs): + raise NotImplementedError("'get_whole_project' not implemented") + + +def _get_subsets( + project_name, + subset_ids=None, + subset_names=None, + folder_ids=None, + names_by_folder_ids=None, + archived=False, + fields=None +): + # Convert fields and add minimum required fields + con = get_server_api_connection() + fields = subset_fields_v3_to_v4(fields, con) + if fields is not None: + for key in ( + "id", + "active" + ): + fields.add(key) + + active = None + if archived: + active = False + + for subset in con.get_subsets( + project_name, + subset_ids, + subset_names, + folder_ids, + names_by_folder_ids, + active, + fields + ): + yield convert_v4_subset_to_v3(subset) + + +def _get_versions( + project_name, + version_ids=None, + subset_ids=None, + versions=None, + hero=True, + standard=True, + latest=None, + fields=None +): + con = get_server_api_connection() + + fields = version_fields_v3_to_v4(fields, con) + + # Make sure 'subsetId' and 'version' are available when hero versions + # are queried + if fields and hero: + fields = set(fields) + fields |= {"subsetId", "version"} + + queried_versions = con.get_versions( + project_name, + version_ids, + subset_ids, + versions, + hero, + standard, + latest, + fields=fields + ) + + versions = [] + hero_versions = [] + for version in queried_versions: + if version["version"] < 0: + hero_versions.append(version) + else: + versions.append(convert_v4_version_to_v3(version)) + + if hero_versions: + subset_ids = set() + versions_nums = set() + for hero_version in hero_versions: + versions_nums.add(abs(hero_version["version"])) + subset_ids.add(hero_version["subsetId"]) + + hero_eq_versions = con.get_versions( + project_name, + subset_ids=subset_ids, + versions=versions_nums, + hero=False, + fields=["id", "version", "subsetId"] + ) + hero_eq_by_subset_id = collections.defaultdict(list) + for version in hero_eq_versions: + hero_eq_by_subset_id[version["subsetId"]].append(version) + + for hero_version in hero_versions: + abs_version = abs(hero_version["version"]) + subset_id = hero_version["subsetId"] + version_id = None + for version in hero_eq_by_subset_id.get(subset_id, []): + if version["version"] == abs_version: + version_id = version["id"] + break + conv_hero = convert_v4_version_to_v3(hero_version) + conv_hero["version_id"] = version_id + versions.append(conv_hero) + + return versions + + +def get_asset_by_id(project_name, asset_id, fields=None): + assets = get_assets( + project_name, asset_ids=[asset_id], fields=fields + ) + for asset in assets: + return asset + return None + + +def get_asset_by_name(project_name, asset_name, fields=None): + assets = get_assets( + project_name, asset_names=[asset_name], fields=fields + ) + for asset in assets: + return asset + return None + + +def get_assets( + project_name, + asset_ids=None, + asset_names=None, + parent_ids=None, + archived=False, + fields=None +): + if not project_name: + return + + active = True + if archived: + active = False + + con = get_server_api_connection() + fields = folder_fields_v3_to_v4(fields, con) + kwargs = dict( + folder_ids=asset_ids, + folder_names=asset_names, + parent_ids=parent_ids, + active=active, + fields=fields + ) + + if fields is None or "tasks" in fields: + folders = get_folders_with_tasks(con, project_name, **kwargs) + + else: + folders = con.get_folders(project_name, **kwargs) + + for folder in folders: + yield convert_v4_folder_to_v3(folder, project_name) + + +def get_archived_assets(*args, **kwargs): + raise NotImplementedError("'get_archived_assets' not implemented") + + +def get_asset_ids_with_subsets(project_name, asset_ids=None): + con = get_server_api_connection() + return con.get_asset_ids_with_subsets(project_name, asset_ids) + + +def get_subset_by_id(project_name, subset_id, fields=None): + subsets = get_subsets( + project_name, subset_ids=[subset_id], fields=fields + ) + for subset in subsets: + return subset + return None + + +def get_subset_by_name(project_name, subset_name, asset_id, fields=None): + subsets = get_subsets( + project_name, + subset_names=[subset_name], + asset_ids=[asset_id], + fields=fields + ) + for subset in subsets: + return subset + return None + + +def get_subsets( + project_name, + subset_ids=None, + subset_names=None, + asset_ids=None, + names_by_asset_ids=None, + archived=False, + fields=None +): + return _get_subsets( + project_name, + subset_ids, + subset_names, + asset_ids, + names_by_asset_ids, + archived, + fields=fields + ) + + +def get_subset_families(project_name, subset_ids=None): + con = get_server_api_connection() + return con.get_subset_families(project_name, subset_ids) + + +def get_version_by_id(project_name, version_id, fields=None): + versions = get_versions( + project_name, + version_ids=[version_id], + fields=fields, + hero=True + ) + for version in versions: + return version + return None + + +def get_version_by_name(project_name, version, subset_id, fields=None): + versions = get_versions( + project_name, + subset_ids=[subset_id], + versions=[version], + fields=fields + ) + for version in versions: + return version + return None + + +def get_versions( + project_name, + version_ids=None, + subset_ids=None, + versions=None, + hero=False, + fields=None +): + return _get_versions( + project_name, + version_ids, + subset_ids, + versions, + hero=hero, + standard=True, + fields=fields + ) + + +def get_hero_version_by_id(project_name, version_id, fields=None): + versions = get_hero_versions( + project_name, + version_ids=[version_id], + fields=fields + ) + for version in versions: + return version + return None + + +def get_hero_version_by_subset_id( + project_name, subset_id, fields=None +): + versions = get_hero_versions( + project_name, + subset_ids=[subset_id], + fields=fields + ) + for version in versions: + return version + return None + + +def get_hero_versions( + project_name, subset_ids=None, version_ids=None, fields=None +): + return _get_versions( + project_name, + version_ids=version_ids, + subset_ids=subset_ids, + hero=True, + standard=False, + fields=fields + ) + + +def get_last_versions(project_name, subset_ids, fields=None): + if fields: + fields = set(fields) + fields.add("parent") + + versions = _get_versions( + project_name, + subset_ids=subset_ids, + latest=True, + hero=False, + fields=fields + ) + return { + version["parent"]: version + for version in versions + } + + +def get_last_version_by_subset_id(project_name, subset_id, fields=None): + versions = _get_versions( + project_name, + subset_ids=[subset_id], + latest=True, + hero=False, + fields=fields + ) + if not versions: + return versions[0] + return None + + +def get_last_version_by_subset_name( + project_name, + subset_name, + asset_id=None, + asset_name=None, + fields=None +): + if not asset_id and not asset_name: + return None + + if not asset_id: + asset = get_asset_by_name( + project_name, asset_name, fields=["_id"] + ) + if not asset: + return None + asset_id = asset["_id"] + + subset = get_subset_by_name( + project_name, subset_name, asset_id, fields=["_id"] + ) + if not subset: + return None + return get_last_version_by_subset_id( + project_name, subset["id"], fields=fields + ) + + +def get_output_link_versions(*args, **kwargs): + raise NotImplementedError("'get_output_link_versions' not implemented") + + +def version_is_latest(project_name, version_id): + con = get_server_api_connection() + return con.version_is_latest(project_name, version_id) + + +def get_representation_by_id(project_name, representation_id, fields=None): + representations = get_representations( + project_name, + representation_ids=[representation_id], + fields=fields + ) + for representation in representations: + return representation + return None + + +def get_representation_by_name( + project_name, representation_name, version_id, fields=None +): + representations = get_representations( + project_name, + representation_names=[representation_name], + version_ids=[version_id], + fields=fields + ) + for representation in representations: + return representation + return None + + +def get_representations( + project_name, + representation_ids=None, + representation_names=None, + version_ids=None, + context_filters=None, + names_by_version_ids=None, + archived=False, + standard=True, + fields=None +): + if context_filters is not None: + # TODO should we add the support? + # - there was ability to fitler using regex + raise ValueError("OP v4 can't filter by representation context.") + + if not archived and not standard: + return + + if archived and not standard: + active = False + elif not archived and standard: + active = True + else: + active = None + + con = get_server_api_connection() + fields = representation_fields_v3_to_v4(fields, con) + if fields and active is not None: + fields.add("active") + + representations = con.get_representations( + project_name, + representation_ids, + representation_names, + version_ids, + names_by_version_ids, + active, + fields=fields + ) + for representation in representations: + yield convert_v4_representation_to_v3(representation) + + +def get_representation_parents(project_name, representation): + if not representation: + return None + + repre_id = representation["_id"] + parents_by_repre_id = get_representations_parents( + project_name, [representation] + ) + return parents_by_repre_id[repre_id] + + +def get_representations_parents(project_name, representations): + repre_ids = { + repre["_id"] + for repre in representations + } + con = get_server_api_connection() + parents_by_repre_id = con.get_representations_parents(project_name, + repre_ids) + folder_ids = set() + for parents in parents_by_repre_id .values(): + folder_ids.add(parents[2]["id"]) + + tasks_by_folder_id = {} + + new_parents = {} + for repre_id, parents in parents_by_repre_id .items(): + version, subset, folder, project = parents + folder_tasks = tasks_by_folder_id.get(folder["id"]) or {} + folder["tasks"] = folder_tasks + new_parents[repre_id] = ( + convert_v4_version_to_v3(version), + convert_v4_subset_to_v3(subset), + convert_v4_folder_to_v3(folder, project_name), + project + ) + return new_parents + + +def get_archived_representations( + project_name, + representation_ids=None, + representation_names=None, + version_ids=None, + context_filters=None, + names_by_version_ids=None, + fields=None +): + return get_representations( + project_name, + representation_ids=representation_ids, + representation_names=representation_names, + version_ids=version_ids, + context_filters=context_filters, + names_by_version_ids=names_by_version_ids, + archived=True, + standard=False, + fields=fields + ) + + +def get_thumbnail( + project_name, thumbnail_id, entity_type, entity_id, fields=None +): + """Receive thumbnail entity data. + + Args: + project_name (str): Name of project where to look for queried entities. + thumbnail_id (Union[str, ObjectId]): Id of thumbnail entity. + entity_type (str): Type of entity for which the thumbnail should be + received. + entity_id (str): Id of entity for which the thumbnail should be + received. + fields (Iterable[str]): Fields that should be returned. All fields are + returned if 'None' is passed. + + Returns: + None: If thumbnail with specified id was not found. + Dict: Thumbnail entity data which can be reduced to specified 'fields'. + """ + + if not thumbnail_id or not entity_type or not entity_id: + return None + + if entity_type == "asset": + entity_type = "folder" + + elif entity_type == "hero_version": + entity_type = "version" + + return { + "_id": thumbnail_id, + "type": "thumbnail", + "schema": CURRENT_THUMBNAIL_SCHEMA, + "data": { + "entity_type": entity_type, + "entity_id": entity_id + } + } + + +def get_thumbnails(project_name, thumbnail_contexts, fields=None): + thumbnail_items = set() + for thumbnail_context in thumbnail_contexts: + thumbnail_id, entity_type, entity_id = thumbnail_context + thumbnail_item = get_thumbnail( + project_name, thumbnail_id, entity_type, entity_id + ) + if thumbnail_item: + thumbnail_items.add(thumbnail_item) + return list(thumbnail_items) + + +def get_thumbnail_id_from_source(project_name, src_type, src_id): + """Receive thumbnail id from source entity. + + Args: + project_name (str): Name of project where to look for queried entities. + src_type (str): Type of source entity ('asset', 'version'). + src_id (Union[str, ObjectId]): Id of source entity. + + Returns: + ObjectId: Thumbnail id assigned to entity. + None: If Source entity does not have any thumbnail id assigned. + """ + + if not src_type or not src_id: + return None + + if src_type == "version": + version = get_version_by_id( + project_name, src_id, fields=["data.thumbnail_id"] + ) or {} + return version.get("data", {}).get("thumbnail_id") + + if src_type == "asset": + asset = get_asset_by_id( + project_name, src_id, fields=["data.thumbnail_id"] + ) or {} + return asset.get("data", {}).get("thumbnail_id") + + return None + + +def get_workfile_info( + project_name, asset_id, task_name, filename, fields=None +): + if not asset_id or not task_name or not filename: + return None + + con = get_server_api_connection() + task = con.get_task_by_name( + project_name, asset_id, task_name, fields=["id", "name", "folderId"] + ) + if not task: + return None + + fields = workfile_info_fields_v3_to_v4(fields) + + for workfile_info in con.get_workfiles_info( + project_name, task_ids=[task["id"]], fields=fields + ): + if workfile_info["name"] == filename: + return convert_v4_workfile_info_to_v3(workfile_info, task) + return None diff --git a/openpype/client/server/entity_links.py b/openpype/client/server/entity_links.py new file mode 100644 index 0000000000..f61b461f38 --- /dev/null +++ b/openpype/client/server/entity_links.py @@ -0,0 +1,65 @@ +def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None): + """Extract linked asset ids from asset document. + + One of asset document or asset id must be passed. + + Note: + Asset links now works only from asset to assets. + + Args: + project_name (str): Project where to look for asset. + asset_doc (dict): Asset document from DB. + asset_id (str): Asset id to find its document. + + Returns: + List[Union[ObjectId, str]]: Asset ids of input links. + """ + + return [] + + +def get_linked_assets( + project_name, asset_doc=None, asset_id=None, fields=None +): + """Return linked assets based on passed asset document. + + One of asset document or asset id must be passed. + + Args: + project_name (str): Name of project where to look for queried entities. + asset_doc (Dict[str, Any]): Asset document from database. + asset_id (Union[ObjectId, str]): Asset id. Can be used instead of + asset document. + fields (Iterable[str]): Fields that should be returned. All fields are + returned if 'None' is passed. + + Returns: + List[Dict[str, Any]]: Asset documents of input links for passed + asset doc. + """ + + return [] + + +def get_linked_representation_id( + project_name, repre_doc=None, repre_id=None, link_type=None, max_depth=None +): + """Returns list of linked ids of particular type (if provided). + + One of representation document or representation id must be passed. + Note: + Representation links now works only from representation through version + back to representations. + + Args: + project_name (str): Name of project where look for links. + repre_doc (Dict[str, Any]): Representation document. + repre_id (Union[ObjectId, str]): Representation id. + link_type (str): Type of link (e.g. 'reference', ...). + max_depth (int): Limit recursion level. Default: 0 + + Returns: + List[ObjectId] Linked representation ids. + """ + + return [] diff --git a/openpype/client/server/openpype_comp.py b/openpype/client/server/openpype_comp.py new file mode 100644 index 0000000000..00ee0aae92 --- /dev/null +++ b/openpype/client/server/openpype_comp.py @@ -0,0 +1,156 @@ +import collections +from ayon_api.graphql import GraphQlQuery, FIELD_VALUE, fields_to_dict + +from .constants import DEFAULT_FOLDER_FIELDS + + +def folders_tasks_graphql_query(fields): + query = GraphQlQuery("FoldersQuery") + project_name_var = query.add_variable("projectName", "String!") + folder_ids_var = query.add_variable("folderIds", "[String!]") + parent_folder_ids_var = query.add_variable("parentFolderIds", "[String!]") + folder_paths_var = query.add_variable("folderPaths", "[String!]") + folder_names_var = query.add_variable("folderNames", "[String!]") + has_subsets_var = query.add_variable("folderHasSubsets", "Boolean!") + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + folders_field = project_field.add_field("folders", has_edges=True) + folders_field.set_filter("ids", folder_ids_var) + folders_field.set_filter("parentIds", parent_folder_ids_var) + folders_field.set_filter("names", folder_names_var) + folders_field.set_filter("paths", folder_paths_var) + folders_field.set_filter("hasSubsets", has_subsets_var) + + fields = set(fields) + fields.discard("tasks") + tasks_field = folders_field.add_field("tasks", has_edges=True) + tasks_field.add_field("name") + tasks_field.add_field("taskType") + + nested_fields = fields_to_dict(fields) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, folders_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 + + +def get_folders_with_tasks( + con, + project_name, + folder_ids=None, + folder_paths=None, + folder_names=None, + parent_ids=None, + active=True, + fields=None +): + """Query folders with tasks from server. + + This is for v4 compatibility where tasks were stored on assets. This is + an inefficient way how folders and tasks are queried so it was added only + as compatibility function. + + Todos: + Folder name won't be unique identifier, so we should add folder path + filtering. + + Notes: + Filter 'active' don't have direct filter in GraphQl. + + Args: + con (ServerAPI): Connection to server. + project_name (str): Name of project where folders are. + folder_ids (Iterable[str]): Folder ids to filter. + folder_paths (Iterable[str]): Folder paths used for filtering. + folder_names (Iterable[str]): Folder names used for filtering. + parent_ids (Iterable[str]): Ids of folder parents. Use 'None' + if folder is direct child of project. + active (Union[bool, None]): Filter active/inactive folders. Both + are returned if is set to None. + fields (Union[Iterable(str), None]): Fields to be queried + for folder. All possible folder fields are returned if 'None' + is passed. + + Returns: + List[Dict[str, Any]]: Queried folder entities. + """ + + if not project_name: + return [] + + filters = { + "projectName": project_name + } + if folder_ids is not None: + folder_ids = set(folder_ids) + if not folder_ids: + return [] + filters["folderIds"] = list(folder_ids) + + if folder_paths is not None: + folder_paths = set(folder_paths) + if not folder_paths: + return [] + filters["folderPaths"] = list(folder_paths) + + if folder_names is not None: + folder_names = set(folder_names) + if not folder_names: + return [] + filters["folderNames"] = list(folder_names) + + if parent_ids is not None: + parent_ids = set(parent_ids) + if not parent_ids: + return [] + if None in parent_ids: + # Replace 'None' with '"root"' which is used during GraphQl + # query for parent ids filter for folders without folder + # parent + parent_ids.remove(None) + parent_ids.add("root") + + if project_name in parent_ids: + # Replace project name with '"root"' which is used during + # GraphQl query for parent ids filter for folders without + # folder parent + parent_ids.remove(project_name) + parent_ids.add("root") + + filters["parentFolderIds"] = list(parent_ids) + + if fields: + fields = set(fields) + else: + fields = con.get_default_fields_for_type("folder") + fields |= DEFAULT_FOLDER_FIELDS + + if active is not None: + fields.add("active") + + query = folders_tasks_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + parsed_data = query.query(con) + folders = parsed_data["project"]["folders"] + if active is None: + return folders + return [ + folder + for folder in folders + if folder["active"] is active + ] diff --git a/openpype/client/server/operations.py b/openpype/client/server/operations.py new file mode 100644 index 0000000000..6148f6a098 --- /dev/null +++ b/openpype/client/server/operations.py @@ -0,0 +1,863 @@ +import copy +import json +import collections +import uuid +import datetime + +from bson.objectid import ObjectId +from ayon_api import get_server_api_connection + +from openpype.client.operations_base import ( + REMOVED_VALUE, + CreateOperation, + UpdateOperation, + DeleteOperation, + BaseOperationsSession +) + +from openpype.client.mongo.operations import ( + CURRENT_THUMBNAIL_SCHEMA, + CURRENT_REPRESENTATION_SCHEMA, + CURRENT_HERO_VERSION_SCHEMA, + CURRENT_VERSION_SCHEMA, + CURRENT_SUBSET_SCHEMA, + CURRENT_ASSET_DOC_SCHEMA, + CURRENT_PROJECT_SCHEMA, +) + +from .conversion_utils import ( + convert_create_asset_to_v4, + convert_create_task_to_v4, + convert_create_subset_to_v4, + convert_create_version_to_v4, + convert_create_hero_version_to_v4, + convert_create_representation_to_v4, + convert_create_workfile_info_to_v4, + + convert_update_folder_to_v4, + convert_update_subset_to_v4, + convert_update_version_to_v4, + convert_update_hero_version_to_v4, + convert_update_representation_to_v4, + convert_update_workfile_info_to_v4, +) +from .utils import create_entity_id + + +def _create_or_convert_to_id(entity_id=None): + if entity_id is None: + return create_entity_id() + + if isinstance(entity_id, ObjectId): + raise TypeError("Type of 'ObjectId' is not supported anymore.") + + # Validate if can be converted to uuid + uuid.UUID(entity_id) + return entity_id + + +def new_project_document( + project_name, project_code, config, data=None, entity_id=None +): + """Create skeleton data of project document. + + Args: + project_name (str): Name of project. Used as identifier of a project. + project_code (str): Shorter version of projet without spaces and + special characters (in most of cases). Should be also considered + as unique name across projects. + config (Dic[str, Any]): Project config consist of roots, templates, + applications and other project Anatomy related data. + data (Dict[str, Any]): Project data with information about it's + attributes (e.g. 'fps' etc.) or integration specific keys. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of project document. + """ + + if data is None: + data = {} + + data["code"] = project_code + + return { + "_id": _create_or_convert_to_id(entity_id), + "name": project_name, + "type": CURRENT_PROJECT_SCHEMA, + "entity_data": data, + "config": config + } + + +def new_asset_document( + name, project_id, parent_id, parents, data=None, entity_id=None +): + """Create skeleton data of asset document. + + Args: + name (str): Is considered as unique identifier of asset in project. + project_id (Union[str, ObjectId]): Id of project doument. + parent_id (Union[str, ObjectId]): Id of parent asset. + parents (List[str]): List of parent assets names. + data (Dict[str, Any]): Asset document data. Empty dictionary is used + if not passed. Value of 'parent_id' is used to fill 'visualParent'. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of asset document. + """ + + if data is None: + data = {} + if parent_id is not None: + parent_id = _create_or_convert_to_id(parent_id) + data["visualParent"] = parent_id + data["parents"] = parents + + return { + "_id": _create_or_convert_to_id(entity_id), + "type": "asset", + "name": name, + # This will be ignored + "parent": project_id, + "data": data, + "schema": CURRENT_ASSET_DOC_SCHEMA + } + + +def new_subset_document(name, family, asset_id, data=None, entity_id=None): + """Create skeleton data of subset document. + + Args: + name (str): Is considered as unique identifier of subset under asset. + family (str): Subset's family. + asset_id (Union[str, ObjectId]): Id of parent asset. + data (Dict[str, Any]): Subset document data. Empty dictionary is used + if not passed. Value of 'family' is used to fill 'family'. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of subset document. + """ + + if data is None: + data = {} + data["family"] = family + return { + "_id": _create_or_convert_to_id(entity_id), + "schema": CURRENT_SUBSET_SCHEMA, + "type": "subset", + "name": name, + "data": data, + "parent": _create_or_convert_to_id(asset_id) + } + + +def new_version_doc(version, subset_id, data=None, entity_id=None): + """Create skeleton data of version document. + + Args: + version (int): Is considered as unique identifier of version + under subset. + subset_id (Union[str, ObjectId]): Id of parent subset. + data (Dict[str, Any]): Version document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version document. + """ + + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_id(entity_id), + "schema": CURRENT_VERSION_SCHEMA, + "type": "version", + "name": int(version), + "parent": _create_or_convert_to_id(subset_id), + "data": data + } + + +def new_hero_version_doc(subset_id, data, version=None, entity_id=None): + """Create skeleton data of hero version document. + + Args: + subset_id (Union[str, ObjectId]): Id of parent subset. + data (Dict[str, Any]): Version document data. + version (int): Version of source version. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version document. + """ + + if version is None: + version = -1 + elif version > 0: + version = -version + + return { + "_id": _create_or_convert_to_id(entity_id), + "schema": CURRENT_HERO_VERSION_SCHEMA, + "type": "hero_version", + "version": version, + "parent": _create_or_convert_to_id(subset_id), + "data": data + } + + +def new_representation_doc( + name, version_id, context, data=None, entity_id=None +): + """Create skeleton data of representation document. + + Args: + name (str): Representation name considered as unique identifier + of representation under version. + version_id (Union[str, ObjectId]): Id of parent version. + context (Dict[str, Any]): Representation context used for fill template + of to query. + data (Dict[str, Any]): Representation document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version document. + """ + + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_id(entity_id), + "schema": CURRENT_REPRESENTATION_SCHEMA, + "type": "representation", + "parent": _create_or_convert_to_id(version_id), + "name": name, + "data": data, + + # Imprint shortcut to context for performance reasons. + "context": context + } + + +def new_thumbnail_doc(data=None, entity_id=None): + """Create skeleton data of thumbnail document. + + Args: + data (Dict[str, Any]): Thumbnail document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of thumbnail document. + """ + + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_id(entity_id), + "type": "thumbnail", + "schema": CURRENT_THUMBNAIL_SCHEMA, + "data": data + } + + +def new_workfile_info_doc( + filename, asset_id, task_name, files, data=None, entity_id=None +): + """Create skeleton data of workfile info document. + + Workfile document is at this moment used primarily for artist notes. + + Args: + filename (str): Filename of workfile. + asset_id (Union[str, ObjectId]): Id of asset under which workfile live. + task_name (str): Task under which was workfile created. + files (List[str]): List of rootless filepaths related to workfile. + data (Dict[str, Any]): Additional metadata. + + Returns: + Dict[str, Any]: Skeleton of workfile info document. + """ + + if not data: + data = {} + + return { + "_id": _create_or_convert_to_id(entity_id), + "type": "workfile", + "parent": _create_or_convert_to_id(asset_id), + "task_name": task_name, + "filename": filename, + "data": data, + "files": files + } + + +def _prepare_update_data(old_doc, new_doc, replace): + changes = {} + for key, value in new_doc.items(): + if key not in old_doc or value != old_doc[key]: + changes[key] = value + + if replace: + for key in old_doc.keys(): + if key not in new_doc: + changes[key] = REMOVED_VALUE + return changes + + +def prepare_subset_update_data(old_doc, new_doc, replace=True): + """Compare two subset documents and prepare update data. + + Based on compared values will create update data for + 'MongoUpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_version_update_data(old_doc, new_doc, replace=True): + """Compare two version documents and prepare update data. + + Based on compared values will create update data for + 'MongoUpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_hero_version_update_data(old_doc, new_doc, replace=True): + """Compare two hero version documents and prepare update data. + + Based on compared values will create update data for 'UpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_representation_update_data(old_doc, new_doc, replace=True): + """Compare two representation documents and prepare update data. + + Based on compared values will create update data for + 'MongoUpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_workfile_info_update_data(old_doc, new_doc, replace=True): + """Compare two workfile info documents and prepare update data. + + Based on compared values will create update data for + 'MongoUpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + +class FailedOperations(Exception): + pass + + +def entity_data_json_default(value): + if isinstance(value, datetime.datetime): + return int(value.timestamp()) + + raise TypeError( + "Object of type {} is not JSON serializable".format(str(type(value))) + ) + + +def failed_json_default(value): + return "< Failed value {} > {}".format(type(value), str(value)) + + +class ServerCreateOperation(CreateOperation): + """Opeartion to create an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + data (Dict[str, Any]): Data of entity that will be created. + """ + + def __init__(self, project_name, entity_type, data, session): + self._session = session + + if not data: + data = {} + data = copy.deepcopy(data) + if entity_type == "project": + raise ValueError("Project cannot be created using operations") + + tasks = None + if entity_type in "asset": + # TODO handle tasks + entity_type = "folder" + if "data" in data: + tasks = data["data"].get("tasks") + + project = self._session.get_project(project_name) + new_data = convert_create_asset_to_v4(data, project, self.con) + + elif entity_type == "task": + project = self._session.get_project(project_name) + new_data = convert_create_task_to_v4(data, project, self.con) + + elif entity_type == "subset": + new_data = convert_create_subset_to_v4(data, self.con) + + elif entity_type == "version": + new_data = convert_create_version_to_v4(data, self.con) + + elif entity_type == "hero_version": + new_data = convert_create_hero_version_to_v4( + data, project_name, self.con + ) + entity_type = "version" + + elif entity_type in ("representation", "archived_representation"): + new_data = convert_create_representation_to_v4(data, self.con) + entity_type = "representation" + + elif entity_type == "workfile": + new_data = convert_create_workfile_info_to_v4( + data, project_name, self.con + ) + + else: + raise ValueError( + "Unhandled entity type \"{}\"".format(entity_type) + ) + + # Simple check if data can be dumped into json + # - should raise error on 'ObjectId' object + try: + new_data = json.loads( + json.dumps(new_data, default=entity_data_json_default) + ) + + except: + raise ValueError("Couldn't json parse body: {}".format( + json.dumps(new_data, default=failed_json_default) + )) + + super(ServerCreateOperation, self).__init__( + project_name, entity_type, new_data + ) + + if "id" not in self._data: + self._data["id"] = create_entity_id() + + if tasks: + copied_tasks = copy.deepcopy(tasks) + for task_name, task in copied_tasks.items(): + task["name"] = task_name + task["folderId"] = self._data["id"] + self.session.create_entity( + project_name, "task", task, nested_id=self.id + ) + + @property + def con(self): + return self.session.con + + @property + def session(self): + return self._session + + @property + def entity_id(self): + return self._data["id"] + + def to_server_operation(self): + return { + "id": self.id, + "type": "create", + "entityType": self.entity_type, + "entityId": self.entity_id, + "data": self._data + } + + +class ServerUpdateOperation(UpdateOperation): + """Operation to update an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + entity_id (Union[str, ObjectId]): Identifier of an entity. + update_data (Dict[str, Any]): Key -> value changes that will be set in + database. If value is set to 'REMOVED_VALUE' the key will be + removed. Only first level of dictionary is checked (on purpose). + """ + + def __init__( + self, project_name, entity_type, entity_id, update_data, session + ): + self._session = session + + update_data = copy.deepcopy(update_data) + if entity_type == "project": + raise ValueError("Project cannot be created using operations") + + if entity_type in ("asset", "archived_asset"): + new_update_data = convert_update_folder_to_v4( + project_name, entity_id, update_data, self.con + ) + entity_type = "folder" + + elif entity_type == "subset": + new_update_data = convert_update_subset_to_v4( + project_name, entity_id, update_data, self.con + ) + + elif entity_type == "version": + new_update_data = convert_update_version_to_v4( + project_name, entity_id, update_data, self.con + ) + + elif entity_type == "hero_version": + new_update_data = convert_update_hero_version_to_v4( + project_name, entity_id, update_data, self.con + ) + entity_type = "version" + + elif entity_type in ("representation", "archived_representation"): + new_update_data = convert_update_representation_to_v4( + project_name, entity_id, update_data, self.con + ) + entity_type = "representation" + + elif entity_type == "workfile": + new_update_data = convert_update_workfile_info_to_v4( + project_name, entity_id, update_data, self.con + ) + + else: + raise ValueError( + "Unhandled entity type \"{}\"".format(entity_type) + ) + + try: + new_update_data = json.loads( + json.dumps(new_update_data, default=entity_data_json_default) + ) + + except: + raise ValueError("Couldn't json parse body: {}".format( + json.dumps(new_update_data, default=failed_json_default) + )) + + super(ServerUpdateOperation, self).__init__( + project_name, entity_type, entity_id, new_update_data + ) + + @property + def con(self): + return self.session.con + + @property + def session(self): + return self._session + + def to_server_operation(self): + if not self._update_data: + return None + + update_data = {} + for key, value in self._update_data.items(): + if value is REMOVED_VALUE: + value = None + update_data[key] = value + + return { + "id": self.id, + "type": "update", + "entityType": self.entity_type, + "entityId": self.entity_id, + "data": update_data + } + + +class ServerDeleteOperation(DeleteOperation): + """Opeartion to delete an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + entity_id (Union[str, ObjectId]): Entity id that will be removed. + """ + + def __init__(self, project_name, entity_type, entity_id, session): + self._session = session + + if entity_type == "asset": + entity_type == "folder" + + if entity_type == "hero_version": + entity_type = "version" + + super(ServerDeleteOperation, self).__init__( + project_name, entity_type, entity_id + ) + + @property + def con(self): + return self.session.con + + @property + def session(self): + return self._session + + def to_server_operation(self): + return { + "id": self.id, + "type": self.operation_name, + "entityId": self.entity_id, + "entityType": self.entity_type, + } + + +class OperationsSession(BaseOperationsSession): + def __init__(self, con=None, *args, **kwargs): + super(OperationsSession, self).__init__(*args, **kwargs) + if con is None: + con = get_server_api_connection() + self._con = con + self._project_cache = {} + self._nested_operations = collections.defaultdict(list) + + @property + def con(self): + return self._con + + def get_project(self, project_name): + if project_name not in self._project_cache: + self._project_cache[project_name] = self.con.get_project( + project_name) + return copy.deepcopy(self._project_cache[project_name]) + + def commit(self): + """Commit session operations.""" + + operations, self._operations = self._operations, [] + if not operations: + return + + operations_by_project = collections.defaultdict(list) + for operation in operations: + operations_by_project[operation.project_name].append(operation) + + body_by_id = {} + results = [] + for project_name, operations in operations_by_project.items(): + operations_body = [] + for operation in operations: + body = operation.to_server_operation() + if body is not None: + try: + json.dumps(body) + except: + raise ValueError("Couldn't json parse body: {}".format( + json.dumps( + body, indent=4, default=failed_json_default + ) + )) + + body_by_id[operation.id] = body + operations_body.append(body) + + if operations_body: + result = self._con.post( + "projects/{}/operations".format(project_name), + operations=operations_body, + canFail=False + ) + results.append(result.data) + + for result in results: + if result.get("success"): + continue + + if "operations" not in result: + raise FailedOperations( + "Operation failed. Content: {}".format(str(result)) + ) + + for op_result in result["operations"]: + if not op_result["success"]: + operation_id = op_result["id"] + raise FailedOperations(( + "Operation \"{}\" failed with data:\n{}\nError: {}." + ).format( + operation_id, + json.dumps(body_by_id[operation_id], indent=4), + op_result.get("error", "unknown"), + )) + + def create_entity(self, project_name, entity_type, data, nested_id=None): + """Fast access to 'ServerCreateOperation'. + + Args: + project_name (str): On which project the creation happens. + entity_type (str): Which entity type will be created. + data (Dicst[str, Any]): Entity data. + nested_id (str): Id of other operation from which is triggered + operation -> Operations can trigger suboperations but they + must be added to operations list after it's parent is added. + + Returns: + ServerCreateOperation: Object of update operation. + """ + + operation = ServerCreateOperation( + project_name, entity_type, data, self + ) + + if nested_id: + self._nested_operations[nested_id].append(operation) + else: + self.add(operation) + if operation.id in self._nested_operations: + self.extend(self._nested_operations.pop(operation.id)) + + return operation + + def update_entity( + self, project_name, entity_type, entity_id, update_data, nested_id=None + ): + """Fast access to 'ServerUpdateOperation'. + + Returns: + ServerUpdateOperation: Object of update operation. + """ + + operation = ServerUpdateOperation( + project_name, entity_type, entity_id, update_data, self + ) + if nested_id: + self._nested_operations[nested_id].append(operation) + else: + self.add(operation) + if operation.id in self._nested_operations: + self.extend(self._nested_operations.pop(operation.id)) + return operation + + def delete_entity( + self, project_name, entity_type, entity_id, nested_id=None + ): + """Fast access to 'ServerDeleteOperation'. + + Returns: + ServerDeleteOperation: Object of delete operation. + """ + + operation = ServerDeleteOperation( + project_name, entity_type, entity_id, self + ) + if nested_id: + self._nested_operations[nested_id].append(operation) + else: + self.add(operation) + if operation.id in self._nested_operations: + self.extend(self._nested_operations.pop(operation.id)) + return operation + + +def create_project( + project_name, + project_code, + library_project=False, + preset_name=None, + con=None +): + """Create project using OpenPype settings. + + This project creation function is not validating project document on + creation. It is because project document is created blindly with only + minimum required information about project which is it's name, code, type + and schema. + + Entered project name must be unique and project must not exist yet. + + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. + + Args: + project_name (str): New project name. Should be unique. + project_code (str): Project's code should be unique too. + library_project (bool): Project is library project. + preset_name (str): Name of anatomy preset. Default is used if not + passed. + con (ServerAPI): Connection to server with logged user. + + Raises: + ValueError: When project name already exists in MongoDB. + + Returns: + dict: Created project document. + """ + + if con is None: + con = get_server_api_connection() + + return con.create_project( + project_name, + project_code, + library_project, + preset_name + ) + + +def delete_project(project_name, con=None): + if con is None: + con = get_server_api_connection() + + return con.delete_project(project_name) + + +def create_thumbnail(project_name, src_filepath, con=None): + if con is None: + con = get_server_api_connection() + return con.create_thumbnail(project_name, src_filepath) diff --git a/openpype/client/server/utils.py b/openpype/client/server/utils.py new file mode 100644 index 0000000000..ed128cfad9 --- /dev/null +++ b/openpype/client/server/utils.py @@ -0,0 +1,109 @@ +import uuid + +from openpype.client.operations_base import REMOVED_VALUE + + +def create_entity_id(): + return uuid.uuid1().hex + + +def prepare_attribute_changes(old_entity, new_entity, replace=False): + """Prepare changes of attributes on entities. + + Compare 'attrib' of old and new entity data to prepare only changed + values that should be sent to server for update. + + Example: + >>> # Limited entity data to 'attrib' + >>> old_entity = { + ... "attrib": {"attr_1": 1, "attr_2": "MyString", "attr_3": True} + ... } + >>> new_entity = { + ... "attrib": {"attr_1": 2, "attr_3": True, "attr_4": 3} + ... } + >>> # Changes if replacement should not happen + >>> expected_changes = { + ... "attr_1": 2, + ... "attr_4": 3 + ... } + >>> changes = prepare_attribute_changes(old_entity, new_entity) + >>> changes == expected_changes + True + + >>> # Changes if replacement should happen + >>> expected_changes_replace = { + ... "attr_1": 2, + ... "attr_2": REMOVED_VALUE, + ... "attr_4": 3 + ... } + >>> changes_replace = prepare_attribute_changes( + ... old_entity, new_entity, True) + >>> changes_replace == expected_changes_replace + True + + Args: + old_entity (dict[str, Any]): Data of entity queried from server. + new_entity (dict[str, Any]): Entity data with applied changes. + replace (bool): New entity should fully replace all old entity values. + + Returns: + Dict[str, Any]: Values from new entity only if value has changed. + """ + + attrib_changes = {} + new_attrib = new_entity.get("attrib") + old_attrib = old_entity.get("attrib") + if new_attrib is None: + if not replace: + return attrib_changes + new_attrib = {} + + if old_attrib is None: + return new_attrib + + for attr, new_attr_value in new_attrib.items(): + old_attr_value = old_attrib.get(attr) + if old_attr_value != new_attr_value: + attrib_changes[attr] = new_attr_value + + if replace: + for attr in old_attrib: + if attr not in new_attrib: + attrib_changes[attr] = REMOVED_VALUE + + return attrib_changes + + +def prepare_entity_changes(old_entity, new_entity, replace=False): + """Prepare changes of AYON entities. + + Compare old and new entity to filter values from new data that changed. + + Args: + old_entity (dict[str, Any]): Data of entity queried from server. + new_entity (dict[str, Any]): Entity data with applied changes. + replace (bool): All attributes should be replaced by new values. So + all attribute values that are not on new entity will be removed. + + Returns: + Dict[str, Any]: Only values from new entity that changed. + """ + + changes = {} + for key, new_value in new_entity.items(): + if key == "attrib": + continue + + old_value = old_entity.get(key) + if old_value != new_value: + changes[key] = new_value + + if replace: + for key in old_entity: + if key not in new_entity: + changes[key] = REMOVED_VALUE + + attr_changes = prepare_attribute_changes(old_entity, new_entity, replace) + if attr_changes: + changes["attrib"] = attr_changes + return changes diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 0efc46edaf..f930dec720 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1128,11 +1128,15 @@ def format_anatomy(data): anatomy = Anatomy() log.debug("__ anatomy.templates: {}".format(anatomy.templates)) - padding = int( - anatomy.templates["render"].get( - "frame_padding" + padding = None + if "frame_padding" in anatomy.templates.keys(): + padding = int(anatomy.templates["frame_padding"]) + elif "render" in anatomy.templates.keys(): + padding = int( + anatomy.templates["render"].get( + "frame_padding" + ) ) - ) version = data.get("version", None) if not version: diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 6f52efdfcc..6c1425fc63 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -5,6 +5,8 @@ import platform import json import tempfile +from openpype import AYON_SERVER_ENABLED + from .log import Logger from .vendor_bin_utils import find_executable @@ -321,19 +323,22 @@ def get_openpype_execute_args(*args): It is possible to pass any arguments that will be added after pype executables. """ - pype_executable = os.environ["OPENPYPE_EXECUTABLE"] - pype_args = [pype_executable] + executable = os.environ["OPENPYPE_EXECUTABLE"] + launch_args = [executable] - executable_filename = os.path.basename(pype_executable) + executable_filename = os.path.basename(executable) if "python" in executable_filename.lower(): - pype_args.append( - os.path.join(os.environ["OPENPYPE_ROOT"], "start.py") + filename = "start.py" + if AYON_SERVER_ENABLED: + filename = "ayon_start.py" + launch_args.append( + os.path.join(os.environ["OPENPYPE_ROOT"], filename) ) if args: - pype_args.extend(args) + launch_args.extend(args) - return pype_args + return launch_args def get_linux_launcher_args(*args): diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index c6c9699240..8f09c6be63 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -29,6 +29,7 @@ except ImportError: import six import appdirs +from openpype import AYON_SERVER_ENABLED from openpype.settings import ( get_local_settings, get_system_settings @@ -517,11 +518,54 @@ def _create_local_site_id(registry=None): return new_id +def get_ayon_appdirs(*args): + """Local app data directory of AYON client. + + Args: + *args (Iterable[str]): Subdirectories/files in local app data dir. + + Returns: + str: Path to directory/file in local app data dir. + """ + + return os.path.join( + appdirs.user_data_dir("ayon", "ynput"), + *args + ) + + +def _get_ayon_local_site_id(): + # used for background syncing + site_id = os.environ.get("AYON_SITE_ID") + if site_id: + return site_id + + site_id_path = get_ayon_appdirs("site_id") + if os.path.exists(site_id_path): + with open(site_id_path, "r") as stream: + site_id = stream.read() + + if site_id: + return site_id + + try: + from ayon_common.utils import get_local_site_id as _get_local_site_id + site_id = _get_local_site_id() + except ImportError: + raise ValueError("Couldn't access local site id") + + return site_id + + def get_local_site_id(): """Get local site identifier. Identifier is created if does not exists yet. """ + + if AYON_SERVER_ENABLED: + return _get_ayon_local_site_id() + # override local id from environment # used for background syncing if os.environ.get("OPENPYPE_LOCAL_ID"): diff --git a/openpype/lib/log.py b/openpype/lib/log.py index 26dcd86eec..dc2e6615fe 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -24,6 +24,7 @@ import traceback import threading import copy +from openpype import AYON_SERVER_ENABLED from openpype.client.mongo import ( MongoEnvNotSet, get_default_components, @@ -212,7 +213,7 @@ class Logger: log_mongo_url_components = None # Database name in Mongo - log_database_name = os.environ["OPENPYPE_DATABASE_NAME"] + log_database_name = os.environ.get("OPENPYPE_DATABASE_NAME") # Collection name under database in Mongo log_collection_name = "logs" @@ -326,12 +327,17 @@ class Logger: # Change initialization state to prevent runtime changes # if is executed during runtime cls.initialized = False - cls.log_mongo_url_components = get_default_components() + if not AYON_SERVER_ENABLED: + cls.log_mongo_url_components = get_default_components() # Define if should logging to mongo be used - use_mongo_logging = bool(log4mongo is not None) - if use_mongo_logging: - use_mongo_logging = os.environ.get("OPENPYPE_LOG_TO_SERVER") == "1" + if AYON_SERVER_ENABLED: + use_mongo_logging = False + else: + use_mongo_logging = ( + log4mongo is not None + and os.environ.get("OPENPYPE_LOG_TO_SERVER") == "1" + ) # Set mongo id for process (ONLY ONCE) if use_mongo_logging and cls.mongo_process_id is None: @@ -453,6 +459,9 @@ class Logger: if not cls.use_mongo_logging: return + if not cls.log_database_name: + raise ValueError("Database name for logs is not set") + client = log4mongo.handlers._connection if not client: client = cls.get_log_mongo_connection() diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index 8370ecc88f..2f57d76850 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -5,6 +5,7 @@ import platform import getpass import socket +from openpype import AYON_SERVER_ENABLED from openpype.settings.lib import get_local_settings from .execute import get_openpype_execute_args from .local_settings import get_local_site_id @@ -33,6 +34,21 @@ def get_openpype_info(): } +def get_ayon_info(): + executable_args = get_openpype_execute_args() + if is_running_from_build(): + version_type = "build" + else: + version_type = "code" + return { + "build_verison": get_build_version(), + "version_type": version_type, + "executable": executable_args[-1], + "ayon_root": os.environ["AYON_ROOT"], + "server_url": os.environ["AYON_SERVER_URL"] + } + + def get_workstation_info(): """Basic information about workstation.""" host_name = socket.gethostname() @@ -52,12 +68,17 @@ def get_workstation_info(): def get_all_current_info(): """All information about current process in one dictionary.""" - return { - "pype": get_openpype_info(), + + output = { "workstation": get_workstation_info(), "env": os.environ.copy(), "local_settings": get_local_settings() } + if AYON_SERVER_ENABLED: + output["ayon"] = get_ayon_info() + else: + output["openpype"] = get_openpype_info() + return output def extract_pype_info_to_file(dirpath): diff --git a/openpype/modules/base.py b/openpype/modules/base.py index fb9b4e1096..c1e928ff48 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -12,8 +12,12 @@ import collections import traceback from uuid import uuid4 from abc import ABCMeta, abstractmethod -import six +import six +import appdirs +import ayon_api + +from openpype import AYON_SERVER_ENABLED from openpype.settings import ( get_system_settings, SYSTEM_SETTINGS_KEY, @@ -186,7 +190,11 @@ def get_dynamic_modules_dirs(): Returns: list: Paths loaded from studio overrides. """ + output = [] + if AYON_SERVER_ENABLED: + return output + value = get_studio_system_settings_overrides() for key in ("modules", "addon_paths", platform.system().lower()): if key not in value: @@ -299,6 +307,108 @@ def load_modules(force=False): time.sleep(0.1) +def _get_ayon_addons_information(): + """Receive information about addons to use from server. + + Todos: + Actually ask server for the information. + Allow project name as optional argument to be able to query information + about used addons for specific project. + Returns: + List[Dict[str, Any]]: List of addon information to use. + """ + + return ayon_api.get_addons_info()["addons"] + + +def _load_ayon_addons(openpype_modules, modules_key, log): + """Load AYON addons based on information from server. + + This function should not trigger downloading of any addons but only use + what is already available on the machine (at least in first stages of + development). + + Args: + openpype_modules (_ModuleClass): Module object where modules are + stored. + log (logging.Logger): Logger object. + + Returns: + List[str]: List of v3 addons to skip to load because v4 alternative is + imported. + """ + + v3_addons_to_skip = [] + + addons_info = _get_ayon_addons_information() + if not addons_info: + return v3_addons_to_skip + addons_dir = os.path.join( + appdirs.user_data_dir("ayon", "ynput"), + "addons" + ) + if not os.path.exists(addons_dir): + log.warning("Addons directory does not exists. Path \"{}\"".format( + addons_dir + )) + return v3_addons_to_skip + + for addon_info in addons_info: + addon_name = addon_info["name"] + addon_version = addon_info.get("productionVersion") + if not addon_version: + continue + + folder_name = "{}_{}".format(addon_name, addon_version) + addon_dir = os.path.join(addons_dir, folder_name) + if not os.path.exists(addon_dir): + log.warning(( + "Directory for addon {} {} does not exists. Path \"{}\"" + ).format(addon_name, addon_version, addon_dir)) + continue + + sys.path.insert(0, addon_dir) + imported_modules = [] + for name in os.listdir(addon_dir): + path = os.path.join(addon_dir, name) + basename, ext = os.path.splitext(name) + is_dir = os.path.isdir(path) + is_py_file = ext.lower() == ".py" + if not is_py_file and not is_dir: + continue + + try: + mod = __import__(basename, fromlist=("",)) + imported_modules.append(mod) + except BaseException: + log.warning( + "Failed to import \"{}\"".format(basename), + exc_info=True + ) + + if not imported_modules: + log.warning("Addon {} {} has no content to import".format( + addon_name, addon_version + )) + continue + + if len(imported_modules) == 1: + mod = imported_modules[0] + addon_alias = getattr(mod, "V3_ALIAS", None) + if not addon_alias: + addon_alias = addon_name + v3_addons_to_skip.append(addon_alias) + new_import_str = "{}.{}".format(modules_key, addon_alias) + + sys.modules[new_import_str] = mod + setattr(openpype_modules, addon_alias, mod) + + else: + log.info("More then one module was imported") + + return v3_addons_to_skip + + def _load_modules(): # Key under which will be modules imported in `sys.modules` modules_key = "openpype_modules" @@ -308,6 +418,12 @@ def _load_modules(): log = Logger.get_logger("ModulesLoader") + ignore_addon_names = [] + if AYON_SERVER_ENABLED: + ignore_addon_names = _load_ayon_addons( + openpype_modules, modules_key, log + ) + # Look for OpenPype modules in paths defined with `get_module_dirs` # - dynamically imported OpenPype modules and addons module_dirs = get_module_dirs() @@ -351,6 +467,9 @@ def _load_modules(): fullpath = os.path.join(dirpath, filename) basename, ext = os.path.splitext(filename) + if basename in ignore_addon_names: + continue + # Validations if os.path.isdir(fullpath): # Check existence of init file diff --git a/openpype/modules/log_viewer/log_view_module.py b/openpype/modules/log_viewer/log_view_module.py index e9dba2041c..1cafbe4fbd 100644 --- a/openpype/modules/log_viewer/log_view_module.py +++ b/openpype/modules/log_viewer/log_view_module.py @@ -1,3 +1,4 @@ +from openpype import AYON_SERVER_ENABLED from openpype.modules import OpenPypeModule, ITrayModule @@ -7,6 +8,8 @@ class LogViewModule(OpenPypeModule, ITrayModule): def initialize(self, modules_settings): logging_settings = modules_settings[self.name] self.enabled = logging_settings["enabled"] + if AYON_SERVER_ENABLED: + self.enabled = False # Tray attributes self.window = None diff --git a/openpype/modules/project_manager_action.py b/openpype/modules/project_manager_action.py index 5f74dd9ee5..bf55e1544d 100644 --- a/openpype/modules/project_manager_action.py +++ b/openpype/modules/project_manager_action.py @@ -1,3 +1,4 @@ +from openpype import AYON_SERVER_ENABLED from openpype.modules import OpenPypeModule, ITrayAction @@ -11,6 +12,9 @@ class ProjectManagerAction(OpenPypeModule, ITrayAction): module_settings = modules_settings.get(self.name) if module_settings: enabled = module_settings.get("enabled", enabled) + + if AYON_SERVER_ENABLED: + enabled = False self.enabled = enabled # Tray attributes diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py index 90092a133d..5950fbd910 100644 --- a/openpype/modules/settings_action.py +++ b/openpype/modules/settings_action.py @@ -1,3 +1,4 @@ +from openpype import AYON_SERVER_ENABLED from openpype.modules import OpenPypeModule, ITrayAction @@ -10,6 +11,8 @@ class SettingsAction(OpenPypeModule, ITrayAction): def initialize(self, _modules_settings): # This action is always enabled self.enabled = True + if AYON_SERVER_ENABLED: + self.enabled = False # User role # TODO should be changeable @@ -80,6 +83,8 @@ class LocalSettingsAction(OpenPypeModule, ITrayAction): def initialize(self, _modules_settings): # This action is always enabled self.enabled = True + if AYON_SERVER_ENABLED: + self.enabled = False # Tray attributes self.settings_window = None diff --git a/openpype/plugins/load/add_site.py b/openpype/modules/sync_server/plugins/load/add_site.py similarity index 100% rename from openpype/plugins/load/add_site.py rename to openpype/modules/sync_server/plugins/load/add_site.py diff --git a/openpype/plugins/load/remove_site.py b/openpype/modules/sync_server/plugins/load/remove_site.py similarity index 100% rename from openpype/plugins/load/remove_site.py rename to openpype/modules/sync_server/plugins/load/remove_site.py diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 98065b68a0..1b7b2dc3a6 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -536,8 +536,8 @@ class SyncServerThread(threading.Thread): _site_is_working(self.module, project_name, remote_site, remote_site_config)]): self.log.debug( - "Some of the sites {} - {} is not working properly".format( - local_site, remote_site + "Some of the sites {} - {} in {} is not working properly".format( # noqa + local_site, remote_site, project_name ) ) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index b85b045bd9..67856f0d8e 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -15,7 +15,7 @@ from openpype.client import ( get_representations, get_representation_by_id, ) -from openpype.modules import OpenPypeModule, ITrayModule +from openpype.modules import OpenPypeModule, ITrayModule, IPluginPaths from openpype.settings import ( get_project_settings, get_system_settings, @@ -39,7 +39,7 @@ from .utils import time_function, SyncStatus, SiteAlreadyPresentError log = Logger.get_logger("SyncServer") -class SyncServerModule(OpenPypeModule, ITrayModule): +class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths): """ Synchronization server that is syncing published files from local to any of implemented providers (like GDrive, S3 etc.) @@ -136,6 +136,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # projects that long tasks are running on self.projects_processed = set() + def get_plugin_paths(self): + """Deadline plugin paths.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + return { + "load": [os.path.join(current_dir, "plugins", "load")] + } + """ Start of Public API """ def add_site(self, project_name, representation_id, site_name=None, force=False, priority=None, reset_timer=False): @@ -204,6 +211,58 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if remove_local_files: self._remove_local_file(project_name, representation_id, site_name) + def get_progress_for_repre(self, doc, active_site, remote_site): + """ + Calculates average progress for representation. + If site has created_dt >> fully available >> progress == 1 + Could be calculated in aggregate if it would be too slow + Args: + doc(dict): representation dict + Returns: + (dict) with active and remote sites progress + {'studio': 1.0, 'gdrive': -1} - gdrive site is not present + -1 is used to highlight the site should be added + {'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not + uploaded yet + """ + progress = {active_site: -1, + remote_site: -1} + if not doc: + return progress + + files = {active_site: 0, remote_site: 0} + doc_files = doc.get("files") or [] + for doc_file in doc_files: + if not isinstance(doc_file, dict): + continue + + sites = doc_file.get("sites") or [] + for site in sites: + if ( + # Pype 2 compatibility + not isinstance(site, dict) + # Check if site name is one of progress sites + or site["name"] not in progress + ): + continue + + files[site["name"]] += 1 + norm_progress = max(progress[site["name"]], 0) + if site.get("created_dt"): + progress[site["name"]] = norm_progress + 1 + elif site.get("progress"): + progress[site["name"]] = norm_progress + site["progress"] + else: # site exists, might be failed, do not add again + progress[site["name"]] = 0 + + # for example 13 fully avail. files out of 26 >> 13/26 = 0.5 + avg_progress = {} + avg_progress[active_site] = \ + progress[active_site] / max(files[active_site], 1) + avg_progress[remote_site] = \ + progress[remote_site] / max(files[remote_site], 1) + return avg_progress + def compute_resource_sync_sites(self, project_name): """Get available resource sync sites state for publish process. diff --git a/openpype/pipeline/anatomy.py b/openpype/pipeline/anatomy.py index 30748206a3..029b5cc1ff 100644 --- a/openpype/pipeline/anatomy.py +++ b/openpype/pipeline/anatomy.py @@ -5,17 +5,19 @@ import platform import collections import numbers +import ayon_api import six import time +from openpype import AYON_SERVER_ENABLED from openpype.settings.lib import ( get_local_settings, ) from openpype.settings.constants import ( DEFAULT_PROJECT_KEY ) - from openpype.client import get_project +from openpype.lib import Logger, get_local_site_id from openpype.lib.path_templates import ( TemplateUnsolved, TemplateResult, @@ -23,7 +25,6 @@ from openpype.lib.path_templates import ( TemplatesDict, FormatObject, ) -from openpype.lib.log import Logger from openpype.modules import ModulesManager log = Logger.get_logger(__name__) @@ -475,6 +476,13 @@ class Anatomy(BaseAnatomy): Union[Dict[str, str], None]): Local root overrides. """ + if AYON_SERVER_ENABLED: + if not project_name: + return + return ayon_api.get_project_roots_for_site( + project_name, get_local_site_id() + ) + if local_settings is None: local_settings = get_local_settings() diff --git a/openpype/pipeline/legacy_io.py b/openpype/pipeline/legacy_io.py index bde2b24c2a..60fa035c22 100644 --- a/openpype/pipeline/legacy_io.py +++ b/openpype/pipeline/legacy_io.py @@ -4,6 +4,7 @@ import sys import logging import functools +from openpype import AYON_SERVER_ENABLED from . import schema from .mongodb import AvalonMongoDB, session_data_from_environment @@ -39,8 +40,9 @@ def install(): _connection_object.Session.update(session) _connection_object.install() - module._mongo_client = _connection_object.mongo_client - module._database = module.database = _connection_object.database + if not AYON_SERVER_ENABLED: + module._mongo_client = _connection_object.mongo_client + module._database = module.database = _connection_object.database module._is_installed = True diff --git a/openpype/pipeline/mongodb.py b/openpype/pipeline/mongodb.py index be2b67a5e7..41a44c7373 100644 --- a/openpype/pipeline/mongodb.py +++ b/openpype/pipeline/mongodb.py @@ -5,6 +5,7 @@ import logging import pymongo from uuid import uuid4 +from openpype import AYON_SERVER_ENABLED from openpype.client import OpenPypeMongoConnection from . import schema @@ -187,7 +188,8 @@ class AvalonMongoDB: return self._installed = True - self._database = self.mongo_client[str(os.environ["AVALON_DB"])] + if not AYON_SERVER_ENABLED: + self._database = self.mongo_client[str(os.environ["AVALON_DB"])] def uninstall(self): """Close any connection to the database""" diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index 39f3e17893..9d4a6f3e48 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -2,6 +2,7 @@ import os import copy import logging +from openpype import AYON_SERVER_ENABLED from openpype.client import get_project from . import legacy_io from .anatomy import Anatomy @@ -131,6 +132,32 @@ class BinaryThumbnail(ThumbnailResolver): return thumbnail_entity["data"].get("binary_data") +class ServerThumbnailResolver(ThumbnailResolver): + def process(self, thumbnail_entity, thumbnail_type): + if not AYON_SERVER_ENABLED: + return None + data = thumbnail_entity["data"] + entity_type = data.get("entity_type") + entity_id = data.get("entity_id") + if not entity_type or not entity_id: + return None + + from openpype.client.server.server_api import get_server_api_connection + + project_name = self.dbcon.active_project() + thumbnail_id = thumbnail_entity["_id"] + con = get_server_api_connection() + filepath = con.get_thumbnail( + project_name, entity_type, entity_id, thumbnail_id + ) + content = None + if filepath: + with open(filepath, "rb") as stream: + content = stream.read() + + return content + + # Thumbnail resolvers def discover_thumbnail_resolvers(): return discover(ThumbnailResolver) @@ -146,3 +173,4 @@ def register_thumbnail_resolver_path(path): register_thumbnail_resolver(TemplateResolver) register_thumbnail_resolver(BinaryThumbnail) +register_thumbnail_resolver(ServerThumbnailResolver) diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index 493780645c..1d57545bc0 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -1,6 +1,7 @@ import collections from copy import deepcopy import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_assets, get_archived_assets @@ -16,6 +17,9 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): families = ["clip", "shot"] def process(self, context): + if AYON_SERVER_ENABLED: + return + if "hierarchyContext" not in context.data: self.log.info("skipping IntegrateHierarchyToAvalon") return diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py new file mode 100644 index 0000000000..915650ae41 --- /dev/null +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -0,0 +1,234 @@ +import collections +import copy +import json +import uuid +import pyblish.api + +from ayon_api import slugify_string +from ayon_api.entity_hub import EntityHub + +from openpype import AYON_SERVER_ENABLED + + +def _default_json_parse(value): + return str(value) + + +class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): + """Create entities in AYON based on collected data.""" + + order = pyblish.api.ExtractorOrder - 0.01 + label = "Extract Hierarchy To AYON" + families = ["clip", "shot"] + + def process(self, context): + if not AYON_SERVER_ENABLED: + return + + hierarchy_context = context.data.get("hierarchyContext") + if not hierarchy_context: + self.log.info("Skipping") + return + + project_name = context.data["projectName"] + hierarchy_context = self._filter_hierarchy(context) + if not hierarchy_context: + self.log.info("All folders were filtered out") + return + + self.log.debug("Hierarchy_context: {}".format( + json.dumps(hierarchy_context, default=_default_json_parse) + )) + + entity_hub = EntityHub(project_name) + project = entity_hub.project_entity + + hierarchy_match_queue = collections.deque() + hierarchy_match_queue.append((project, hierarchy_context)) + while hierarchy_match_queue: + item = hierarchy_match_queue.popleft() + entity, entity_info = item + + # Update attributes of entities + for attr_name, attr_value in entity_info["attributes"].items(): + if attr_name in entity.attribs: + entity.attribs[attr_name] = attr_value + + # Check if info has any children to sync + children_info = entity_info["children"] + tasks_info = entity_info["tasks"] + if not tasks_info and not children_info: + continue + + # Prepare children by lowered name to easily find matching entities + children_by_low_name = { + child.name.lower(): child + for child in entity.children + } + + # Create tasks if are not available + for task_info in tasks_info: + task_label = task_info["name"] + task_name = slugify_string(task_label) + if task_name == task_label: + task_label = None + task_entity = children_by_low_name.get(task_name.lower()) + # TODO propagate updates of tasks if there are any + # TODO check if existing entity have 'task' type + if task_entity is None: + task_entity = entity_hub.add_new_task( + task_info["type"], + parent_id=entity.id, + name=task_name + ) + + if task_label: + task_entity.label = task_label + + # Create/Update sub-folders + for child_info in children_info: + child_label = child_info["name"] + child_name = slugify_string(child_label) + if child_name == child_label: + child_label = None + # TODO check if existing entity have 'folder' type + child_entity = children_by_low_name.get(child_name.lower()) + if child_entity is None: + child_entity = entity_hub.add_new_folder( + child_info["entity_type"], + parent_id=entity.id, + name=child_name + ) + + if child_label: + child_entity.label = child_label + + # Add folder to queue + hierarchy_match_queue.append((child_entity, child_info)) + + entity_hub.commit_changes() + + def _filter_hierarchy(self, context): + """Filter hierarchy context by active folder names. + + Hierarchy context is filtered to folder names on active instances. + + Change hierarchy context to unified structure which suits logic in + entity creation. + + Output example: + { + "name": "MyProject", + "entity_type": "Project", + "attributes": {}, + "tasks": [], + "children": [ + { + "name": "seq_01", + "entity_type": "Sequence", + "attributes": {}, + "tasks": [], + "children": [ + ... + ] + }, + ... + ] + } + + Todos: + Change how active folder are defined (names won't be enough in + AYON). + + Args: + context (pyblish.api.Context): Pyblish context. + + Returns: + dict[str, Any]: Hierarchy structure filtered by folder names. + """ + + # filter only the active publishing instances + active_folder_names = set() + for instance in context: + if instance.data.get("publish") is not False: + active_folder_names.add(instance.data.get("asset")) + + active_folder_names.discard(None) + + self.log.debug("Active folder names: {}".format(active_folder_names)) + if not active_folder_names: + return None + + project_item = None + project_children_context = None + for key, value in context.data["hierarchyContext"].items(): + project_item = copy.deepcopy(value) + project_children_context = project_item.pop("childs", None) + project_item["name"] = key + project_item["tasks"] = [] + project_item["attributes"] = project_item.pop( + "custom_attributes", {} + ) + project_item["children"] = [] + + if not project_children_context: + return None + + project_id = uuid.uuid4().hex + items_by_id = {project_id: project_item} + parent_id_by_item_id = {project_id: None} + valid_ids = set() + + hierarchy_queue = collections.deque() + hierarchy_queue.append((project_id, project_children_context)) + while hierarchy_queue: + queue_item = hierarchy_queue.popleft() + parent_id, children_context = queue_item + if not children_context: + continue + + for asset_name, asset_info in children_context.items(): + if ( + asset_name not in active_folder_names + and not asset_info.get("childs") + ): + continue + item_id = uuid.uuid4().hex + new_item = copy.deepcopy(asset_info) + new_item["name"] = asset_name + new_item["children"] = [] + new_children_context = new_item.pop("childs", None) + tasks = new_item.pop("tasks", {}) + task_items = [] + for task_name, task_info in tasks.items(): + task_info["name"] = task_name + task_items.append(task_info) + new_item["tasks"] = task_items + new_item["attributes"] = new_item.pop("custom_attributes", {}) + + items_by_id[item_id] = new_item + parent_id_by_item_id[item_id] = parent_id + + if asset_name in active_folder_names: + valid_ids.add(item_id) + hierarchy_queue.append((item_id, new_children_context)) + + if not valid_ids: + return None + + for item_id in set(valid_ids): + parent_id = parent_id_by_item_id[item_id] + while parent_id is not None and parent_id not in valid_ids: + valid_ids.add(parent_id) + parent_id = parent_id_by_item_id[parent_id] + + valid_ids.discard(project_id) + for item_id in valid_ids: + parent_id = parent_id_by_item_id[item_id] + item = items_by_id[item_id] + parent_item = items_by_id[parent_id] + parent_item["children"].append(item) + + if not project_item["children"]: + return None + return project_item diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index b71207c24f..b7feeac6a4 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -6,6 +6,7 @@ import shutil import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_version_by_id, get_hero_version_by_subset_id, @@ -195,11 +196,20 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): entity_id = None if old_version: entity_id = old_version["_id"] - new_hero_version = new_hero_version_doc( - src_version_entity["_id"], - src_version_entity["parent"], - entity_id=entity_id - ) + + if AYON_SERVER_ENABLED: + new_hero_version = new_hero_version_doc( + src_version_entity["parent"], + copy.deepcopy(src_version_entity["data"]), + src_version_entity["name"], + entity_id=entity_id + ) + else: + new_hero_version = new_hero_version_doc( + src_version_entity["_id"], + src_version_entity["parent"], + entity_id=entity_id + ) if old_version: self.log.debug("Replacing old hero version.") diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 2e87d8fc86..9929d8f754 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -18,6 +18,7 @@ import collections import six import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.client import get_versions from openpype.client.operations import OperationsSession, new_thumbnail_doc from openpype.pipeline.publish import get_publish_instance_label @@ -39,6 +40,10 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): ] def process(self, context): + if AYON_SERVER_ENABLED: + self.log.info("AYON is enabled. Skipping v3 thumbnail integration") + return + # Filter instances which can be used for integration filtered_instance_items = self._prepare_instances(context) if not filtered_instance_items: diff --git a/openpype/plugins/publish/integrate_thumbnail_ayon.py b/openpype/plugins/publish/integrate_thumbnail_ayon.py new file mode 100644 index 0000000000..ba5664c69f --- /dev/null +++ b/openpype/plugins/publish/integrate_thumbnail_ayon.py @@ -0,0 +1,207 @@ +""" Integrate Thumbnails for Openpype use in Loaders. + + This thumbnail is different from 'thumbnail' representation which could + be uploaded to Ftrack, or used as any other representation in Loaders to + pull into a scene. + + This one is used only as image describing content of published item and + shows up only in Loader in right column section. +""" + +import os +import collections + +import pyblish.api + +from openpype import AYON_SERVER_ENABLED +from openpype.client import get_versions +from openpype.client.operations import OperationsSession + +InstanceFilterResult = collections.namedtuple( + "InstanceFilterResult", + ["instance", "thumbnail_path", "version_id"] +) + + +class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): + """Integrate Thumbnails for Openpype use in Loaders.""" + + label = "Integrate Thumbnails to AYON" + order = pyblish.api.IntegratorOrder + 0.01 + + required_context_keys = [ + "project", "asset", "task", "subset", "version" + ] + + def process(self, context): + if not AYON_SERVER_ENABLED: + self.log.info("AYON is not enabled. Skipping") + return + + # Filter instances which can be used for integration + filtered_instance_items = self._prepare_instances(context) + if not filtered_instance_items: + self.log.info( + "All instances were filtered. Thumbnail integration skipped." + ) + return + + project_name = context.data["projectName"] + + # Collect version ids from all filtered instance + version_ids = { + instance_items.version_id + for instance_items in filtered_instance_items + } + # Query versions + version_docs = get_versions( + project_name, + version_ids=version_ids, + hero=True, + fields=["_id", "type", "name"] + ) + # Store version by their id (converted to string) + version_docs_by_str_id = { + str(version_doc["_id"]): version_doc + for version_doc in version_docs + } + self._integrate_thumbnails( + filtered_instance_items, + version_docs_by_str_id, + project_name + ) + + def _prepare_instances(self, context): + context_thumbnail_path = context.get("thumbnailPath") + valid_context_thumbnail = bool( + context_thumbnail_path + and os.path.exists(context_thumbnail_path) + ) + + filtered_instances = [] + for instance in context: + instance_label = self._get_instance_label(instance) + # Skip instances without published representations + # - there is no place where to put the thumbnail + published_repres = instance.data.get("published_representations") + if not published_repres: + self.log.debug(( + "There are no published representations" + " on the instance {}." + ).format(instance_label)) + continue + + # Find thumbnail path on instance + thumbnail_path = self._get_instance_thumbnail_path( + published_repres) + if thumbnail_path: + self.log.debug(( + "Found thumbnail path for instance \"{}\"." + " Thumbnail path: {}" + ).format(instance_label, thumbnail_path)) + + elif valid_context_thumbnail: + # Use context thumbnail path if is available + thumbnail_path = context_thumbnail_path + self.log.debug(( + "Using context thumbnail path for instance \"{}\"." + " Thumbnail path: {}" + ).format(instance_label, thumbnail_path)) + + # Skip instance if thumbnail path is not available for it + if not thumbnail_path: + self.log.info(( + "Skipping thumbnail integration for instance \"{}\"." + " Instance and context" + " thumbnail paths are not available." + ).format(instance_label)) + continue + + version_id = str(self._get_version_id(published_repres)) + filtered_instances.append( + InstanceFilterResult(instance, thumbnail_path, version_id) + ) + return filtered_instances + + def _get_version_id(self, published_representations): + for repre_info in published_representations.values(): + return repre_info["representation"]["parent"] + + def _get_instance_thumbnail_path(self, published_representations): + thumb_repre_doc = None + for repre_info in published_representations.values(): + repre_doc = repre_info["representation"] + if repre_doc["name"].lower() == "thumbnail": + thumb_repre_doc = repre_doc + break + + if thumb_repre_doc is None: + self.log.debug( + "There is not representation with name \"thumbnail\"" + ) + return None + + path = thumb_repre_doc["data"]["path"] + if not os.path.exists(path): + self.log.warning( + "Thumbnail file cannot be found. Path: {}".format(path) + ) + return None + return os.path.normpath(path) + + def _integrate_thumbnails( + self, + filtered_instance_items, + version_docs_by_str_id, + project_name + ): + from openpype.client.server.operations import create_thumbnail + + op_session = OperationsSession() + + for instance_item in filtered_instance_items: + instance, thumbnail_path, version_id = instance_item + instance_label = self._get_instance_label(instance) + version_doc = version_docs_by_str_id.get(version_id) + if not version_doc: + self.log.warning(( + "Version entity for instance \"{}\" was not found." + ).format(instance_label)) + continue + + thumbnail_id = create_thumbnail(project_name, thumbnail_path) + + # Set thumbnail id for version + op_session.update_entity( + project_name, + version_doc["type"], + version_doc["_id"], + {"data.thumbnail_id": thumbnail_id} + ) + if version_doc["type"] == "hero_version": + version_name = "Hero" + else: + version_name = version_doc["name"] + self.log.debug("Setting thumbnail for version \"{}\" <{}>".format( + version_name, version_id + )) + + asset_entity = instance.data["assetEntity"] + op_session.update_entity( + project_name, + asset_entity["type"], + asset_entity["_id"], + {"data.thumbnail_id": thumbnail_id} + ) + self.log.debug("Setting thumbnail for asset \"{}\" <{}>".format( + asset_entity["name"], version_id + )) + + op_session.commit() + + def _get_instance_label(self, instance): + return ( + instance.data.get("label") + or instance.data.get("name") + or "N/A" + ) diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py index 0d7778e546..77cc0deaa2 100644 --- a/openpype/resources/__init__.py +++ b/openpype/resources/__init__.py @@ -1,4 +1,5 @@ import os +from openpype import AYON_SERVER_ENABLED from openpype.lib.openpype_version import is_running_staging RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -40,11 +41,17 @@ def get_liberation_font_path(bold=False, italic=False): def get_openpype_production_icon_filepath(): - return get_resource("icons", "openpype_icon.png") + filename = "openpype_icon.png" + if AYON_SERVER_ENABLED: + filename = "AYON_icon.png" + return get_resource("icons", filename) def get_openpype_staging_icon_filepath(): - return get_resource("icons", "openpype_icon_staging.png") + filename = "openpype_icon_staging.png" + if AYON_SERVER_ENABLED: + filename = "AYON_icon.png" + return get_resource("icons", filename) def get_openpype_icon_filepath(staging=None): @@ -60,7 +67,9 @@ def get_openpype_splash_filepath(staging=None): if staging is None: staging = is_running_staging() - if staging: + if AYON_SERVER_ENABLED: + splash_file_name = "AYON_splash.png" + elif staging: splash_file_name = "openpype_splash_staging.png" else: splash_file_name = "openpype_splash.png" diff --git a/openpype/resources/icons/AYON_icon.png b/openpype/resources/icons/AYON_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ed13aeea527299092c400f18045b11b28c4d23ac GIT binary patch literal 16907 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

+General: Reduce usage of legacy io #4723 + +Replace usages of `legacy_io` with getter methods or reuse already available information. Create plugins using CreateContext are using context from CreateContext object. Loaders are usign getter function from context tools. Publish plugin are using information instance.data or context.data. In some cases were pieces of code refactored a little e.g. fps getter in maya. + + +___ + +
+ + +
+Documentation: API docs reborn - yet again #4419 + +## Feature + +Add functional base for API Documentation using Sphinx and AutoAPI. + +After unsuccessful #2512, #834 and #210 this is yet another try. But this time without ambition to solve the whole issue. This is making Shinx script to work and nothing else. Any changes and improvements in API docs should be made in subsequent PRs. + +## How to use it + +You can run: + +```sh +cd .\docs +make.bat html +``` + +or + +```sh +cd ./docs +make html +``` + +This will go over our code and generate **.rst** files in `/docs/source/autoapi` and from those it will generate full html documentation in `/docs/build/html`. + +During the build you'll see tons of red errors that are pointing to our issues: + +1) **Wrong imports** + Invalid import are usually wrong relative imports (too deep) or circular imports. + +2) **Invalid doc-strings** + Doc-strings to be processed into documentation needs to follow some syntax - this can be checked by running + `pydocstyle` that is already included with OpenPype +3) **Invalid markdown/rst files** + md/rst files can be included inside rst files using `.. include::` directive. But they have to be properly formatted. + + +## Editing rst templates + +Everything starts with `/docs/source/index.rst` - this file should be properly edited, Right now it just includes `readme.rst` that in turn include and parse main `README.md`. This is entrypoint to API documentation. All templates generated by AutoAPI are in `/docs/source/autoapi`. They should be eventually commited to repository and edited too. + +## Steps for enhancing API documentation + +1) Run `/docs/make.bat html` +2) Read the red errors/warnings - fix it in the code +3) Run `/docs/make.bat html` again until there are not red lines +4) Edit rst files and add some meaningfull content there + +> **Note** +> This can (should) be merged as is without doc-string fixes in the code or changes in templates. All additional improvements on API documentation should be made in new PRs. + +> **Warning** +> You need to add new dependencies to use it. Run `create_venv`. + +Connected to #2490 +___ + +
+ + +
+Global: custom location for OP local versions #4673 + +This provides configurable location to unzip Openpype version zips. By default, it was hardcoded to artist's app data folder, which might be problematic/slow with roaming profiles.Location must be accessible by user running OP Tray with write permissions (so `Program Files` might be problematic) + + +___ + +
+ + +
+AYON: Update settings conversion #4837 + +Updated conversion script of AYON settings to v3 settings. PR is related to changes in addons repository https://github.com/ynput/ayon-addons/pull/6 . Changed how the conversion happens -> conversion output does not start with openpype defaults but as empty dictionary. + + +___ + +
+ + +
+AYON: Implement integrate links publish plugin #4842 + +Implemented entity links get/create functions. Added new integrator which replaces v3 integrator for links. + + +___ + +
+ + +
+General: Version attributes integration #4991 + +Implemented unified integrate plugin to update version attributes after all integrations for AYON. The goal is to be able update attribute values in a unified way to a version when all addon integrators are done, so e.g. ftrack can add ftrack id to matching version in AYON server etc.The can be stored under `"versionAttributes"` key. + + +___ + +
+ + +
+AYON: Staging versions can be used #4992 + +Added ability to use staging versions in AYON mode. + + +___ + +
+ + +
+AYON: Preparation for products #5038 + +Prepare ayon settings conversion script for `product` settings conversion. + + +___ + +
+ + +
+Loader: Hide inactive versions in UI #5101 + +Added support for `active` argument to hide versions with active set to False in Loader UI when in AYON mode. + + +___ + +
+ + +
+General: CLI addon command #5109 + +Added `addon` alias for `module` in OpenPype cli commands. + + +___ + +
+ + +
+AYON: OpenPype as server addon #5199 + +OpenPype repository can be converted to AYON addon for distribution. Addon has defined dependencies that are required to use it and are not in base ayon-launcher (desktop application). + + +___ + +
+ + +
+General: Runtime dependencies #5206 + +Defined runtime dependencies in pyproject toml. Moved python ocio and otio modules there. + + +___ + +
+ + +
+AYON: Bundle distribution #5209 + +Since AYON server 0.3.0 are addon versions defined by bundles which affects how addons, dependency packages and installers are handled. Only source of truth, about any version of anything that should be used, is server bundle. + + +___ + +
+ + +
+Feature/blender handle q application #5264 + +This edit is to change the way the QApplication is run for Blender. It calls in the singleton (QApplication) during the register. This is made so that other Qt applications and addons are able to run on Blender. In its current implementation, if a QApplication is already running, all functionality of OpenPype becomes unavailable. + + +___ + +
+ +### **🚀 Enhancements** + + +
+General: Connect to AYON server (base) #3924 + +Initial implementation of being able use AYON server in current OpenPype client. Added ability to connect to AYON server and use base queries. + +AYON mode has it's own executable (and start script). To start in AYON mode just replace `start.py` with `ayon_start.py` (added tray start script to tools). Added constant `AYON_SERVER_ENABLED` to `openpype/__init__.py` to know if ayon mode is enabled. In that case Mongo is not used at all and any attempts will cause crashes.I had to modify `~/openpype/client` content to be able do this switch. Mongo implementation was moved to `mongo` subfolder and use "star imports" in files from where current imports are used. Logic of any tool or query in code was not changed at all. Since functions were based on mongo queries they don't use full potential of AYON server abilities.ATM implementation has login UI, distribution of files from server and replacement of mongo queries. For queries is used `ayon_api` module. Which is in live development so the versions may change from day to day. + + +___ + +
+ + +
+Enhancement kitsu note with exceptions #4537 + +Adding a setting to choose some exceptions to IntegrateKitsuNote task status changes. + + +___ + +
+ + +
+General: Environment variable for default OCIO configs #4670 + +Define environment variable which lead to root of builtin ocio configs to be able change the root without changing settings. For the path in settings was used `"{OPENPYPE_ROOT}/vendor/bin/ocioconfig/OpenColorIOConfig"` which disallow to change the root somewhere else. That will be needed in AYON where configs won't be part of desktop application but downloaded from server. + + +___ + +
+ + +
+AYON: Editorial hierarchy creation #4699 + +Implemented extract hierarchy to AYON plugin which created entities in AYON using ayon api. + + +___ + +
+ + +
+AYON: Vendorize ayon api #4753 + +Vendorize ayon api into openpype vendor directory. The reason is that `ayon-python-api` is in live development and will fix/add features often in next few weeks/months, and because update of dependency requires new release -> new build, we want to avoid the need of doing that as it would affect OpenPype development. + + +___ + +
+ + +
+General: Update PySide 6 for MacOs #4764 + +New version of PySide6 does not have issues with settings UI. It is still breaking UI stylesheets so it is not changed for other plaforms but it is enhancement from previous state. + + +___ + +
+ + +
+General: Removed unused cli commands #4902 + +Removed `texturecopy` and `launch` cli commands from cli commands. + + +___ + +
+ + +
+AYON: Linux & MacOS launch script #4970 + +Added shell script to launch tray in AYON mode. + + +___ + +
+ + +
+General: Qt scale enhancement #5059 + +Set ~~'QT_SCALE_FACTOR_ROUNDING_POLICY'~~ scale factor rounding policy of QApplication to `PassThrough` so the scaling can be 'float' number and not just 'int' (150% -> 1.5 scale). + + +___ + +
+ + +
+CI: WPS linting instead of Hound (rebase) 2 #5115 + +Because Hound currently used to lint the code on GH ships with really old flake8 support, it fails miserably on any newer Python syntax. This PR is adding WPS linter to GitHub workflows that should step in. + + +___ + +
+ + +
+Max: OP parameters only displays what is attached to the container #5229 + +The OP parameter in 3dsmax only displays what is currently attached to the container while deleting while you can see the items which is not added when you are adding to the container. + + +___ + +
+ + +
+Testing: improving logging during testing #5271 + +Unit testing logging was crashing on more then one nested layers of inherited loggers. + + +___ + +
+ + +
+Nuke: removing deprecated settings in baking #5275 + +Removing deprecated settings for baking with reformat. This option was only for single reformat node and it had been substituted with multiple reposition nodes. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+AYON: General fixes and updates #4975 + +Few smaller fixes related to AYON connection. Some of fixes were taken from this PR. + + +___ + +
+ + +
+Start script: Change returncode on validate or list versions #4515 + +Change exit code from `1` to `0` when versions are printed or when version is validated. + +Return code `1` is indicating error but there didn't happen any error. + + +___ + +
+ + +
+AYON: Change login UI works #4754 + +Fixed change of login UI. Logic change UI did show up, new login was successful, but after restart was used the previous login. This change fix the issue. + + +___ + +
+ + +
+AYON: General issues #4763 + +Vendorized `ayon_api` from PR broke OpenPype launch, because `ayon_api` is not available. Moved `ayon_api` from ayon specific subforlder to `common` python vendor in OpenPype, and removed login in ayon start script (which was invalid anyway). Also made fixed compatibility with PySide6 by using `qtpy` instead of `Qt` and changing code which is not PySide6 compatible. + + +___ + +
+ + +
+AYON: Small fixes #4841 + +Bugsfixes and enhancements related to AYON logic. Define `BUILTIN_OCIO_ROOT` environment variable so OCIO configs are working. Use constants from ayon api instead of hardcoding them in codebase. Change process name from "openpype" to "ayon". Don't execute login dialog when application is not yet running but use `open` method instead. Fixed missing modules settings which were not taken from openpype defaults. Updated ayon api to `0.1.17`. + + +___ + +
+ + +
+Bugfix - Update gazu to 0.9.3 #4845 + +This updates Gazu to 0.9.3 to make sure Gazu works with Kitsu and Zou 0.16.x+ + + +___ + +
+ + +
+Igniter: fix error reports in silent mode #4909 + +Some errors in silent mode commands in Igniter were suppressed and not visible for example in Deadline log. + + +___ + +
+ + +
+General: Remove ayon api from poetry lock #4964 + +Remove AYON python api from pyproject.toml and poetry.lock again. + + +___ + +
+ + +
+Ftrack: Fix AYON settings conversion #4967 + +Fix conversion of ftrack settings in AYON mode. + + +___ + +
+ + +
+AYON: ISO date format conversion issues #4981 + +Function `datetime.fromisoformat` was replaced with `arrow.get` to be used instead. + + +___ + +
+ + +
+AYON: Missing files on representations #4989 + +Fix integration of files into representation in server database. + + +___ + +
+ + +
+General: Fix Python 2 vendor for arrow #4993 + +Moved remaining dependencies for arrow from ftrack to python 2 vendor. + + +___ + +
+ + +
+General: Fix new load plugins for next minor relase #5000 + +Fix access to `fname` attribute which is not available on load plugin anymore. + + +___ + +
+ + +
+General: Fix mongo secure connection #5031 + +Fix `ssl` and `tls` keys checks in mongo uri query string. + + +___ + +
+ + +
+AYON: Fix site sync settings #5069 + +Fixed settings for AYON variant of sync server. + + +___ + +
+ + +
+General: Replace deprecated keyword argument in PyMongo #5080 + +Use argument `tlsCAFile` instead of `ssl_ca_certs` to avoid deprecation warnings. + + +___ + +
+ + +
+Igniter: QApplication is created #5081 + +Function `_get_qt_app` actually creates new `QApplication` if was not created yet. + + +___ + +
+ + +
+General: Lower unidecode version #5090 + +Use older version of Unidecode module to support Python 2. + + +___ + +
+ + +
+General: Lower cryptography to 39.0.0 #5099 + +Lower cryptography to 39.0.0 to avoid breaking of DCCs like Maya and Nuke. + + +___ + +
+ + +
+AYON: Global environments key fix #5118 + +Seems that when converting ayon settings to OP settings the `environments` setting is put under the `environments` key in `general` however when populating the environment the `environment` key gets picked up, which does not contain the environment variables from the `core/environments` setting + + +___ + +
+ + +
+Add collector to tray publisher for getting frame range data #5152 + +Add collector to tray publisher to get frame range data. User can choose to enable this collector if they need this in the publisher.Resolve #5136 + + +___ + +
+ + +
+Unreal: get current project settings not using unreal project name #5170 + +There was a bug where Unreal project name was used to query project settings. But Unreal project name can differ from the "real" one because of naming convention rules set by Unreal. This is fixing it by asking for current project settings. + + +___ + +
+ + +
+Substance Painter: Fix Collect Texture Set Images unable to copy.deepcopy due to QMenu #5238 + +Fix `copy.deepcopy` of `instance.data`. + + +___ + +
+ + +
+Ayon: server returns different key #5251 + +Package returned from server has `filename` instead of `name`. + + +___ + +
+ + +
+Substance Painter: Fix default color management settings #5259 + +The default settings for color management for Substance Painter were invalid, it was set to override the global config by default but specified no valid config paths of its own - and thus errored that the paths were not correct.This sets the defaults correctly to match other hosts._I quickly checked - this seems to be the only host with the wrong default settings_ + + +___ + +
+ + +
+Nuke: fixing container data if windows path in value #5267 + +Windows path in container data are reformatted. Previously it was reported that Nuke was rising `utf8 0xc0` error if backward slashes were in data values. + + +___ + +
+ + +
+Houdini: fix typo error in collect arnold rop #5281 + +Fixing a typo error in `collect_arnold_rop.py`Reference: #5280 + + +___ + +
+ + +
+Slack - enhanced logging and protection against failure #5287 + +Covered issues found in production on customer site. SlackAPI exception doesn't need to have 'error', covered uncaught exception. + + +___ + +
+ + +
+Maya: Removed unnecessary import of pyblish.cli #5292 + +This import resulted in adding additional logging handler which lead to duplication of logs in hosts with plugins containing `is_in_tests` method. Import is unnecessary for testing functionality. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Loader: Remove `context` argument from Loader.__init__() #4602 + +Remove the previously required `context` argument. + + +___ + +
+ + +
+Global: Remove legacy integrator #4786 + +Remove the legacy integrator. + + +___ + +
+ +### **📃 Documentation** + + +
+Next Minor Release #5291 + + +___ + +
+ +### **Merged pull requests** + + +
+Maya: Refactor to new publisher #4388 + +**Refactor Maya to use the new publisher with new creators.** + + +- [x] Legacy instance can be converted in UI using `SubsetConvertorPlugin` +- [x] Fix support for old style "render" and "vrayscene" instance to the new per layer format. +- [x] Context data is stored with scene +- [x] Workfile instance converted to AutoCreator +- [x] Converted Creator classes +- [x] Create animation +- [x] Create ass +- [x] Create assembly +- [x] Create camera +- [x] Create layout +- [x] Create look +- [x] Create mayascene +- [x] Create model +- [x] Create multiverse look +- [x] Create multiverse usd +- [x] Create multiverse usd comp +- [x] Create multiverse usd over +- [x] Create pointcache +- [x] Create proxy abc +- [x] Create redshift proxy +- [x] Create render +- [x] Create rendersetup +- [x] Create review +- [x] Create rig +- [x] Create setdress +- [x] Create unreal skeletalmesh +- [x] Create unreal staticmesh +- [x] Create vrayproxy +- [x] Create vrayscene +- [x] Create xgen +- [x] Create yeti cache +- [x] Create yeti rig +- [ ] Tested new Creator publishes +- [x] Publish animation +- [x] Publish ass +- [x] Publish assembly +- [x] Publish camera +- [x] Publish layout +- [x] Publish look +- [x] Publish mayascene +- [x] Publish model +- [ ] Publish multiverse look +- [ ] Publish multiverse usd +- [ ] Publish multiverse usd comp +- [ ] Publish multiverse usd over +- [x] Publish pointcache +- [x] Publish proxy abc +- [x] Publish redshift proxy +- [x] Publish render +- [x] Publish rendersetup +- [x] Publish review +- [x] Publish rig +- [x] Publish setdress +- [x] Publish unreal skeletalmesh +- [x] Publish unreal staticmesh +- [x] Publish vrayproxy +- [x] Publish vrayscene +- [x] Publish xgen +- [x] Publish yeti cache +- [x] Publish yeti rig +- [x] Publish workfile +- [x] Rig loader correctly generates a new style animation creator instance +- [ ] Validations / Error messages for common validation failures look nice and usable as a report. +- [ ] Make Create Animation hidden to the user (should not create manually?) +- [x] Correctly detect difference between **'creator_attributes'** and **'instance_data'** since both are "flattened" to the top node. + + +___ + +
+ + +
+Start script: Fix possible issues with destination drive path #4478 + +Drive paths for windows are fixing possibly missing slash at the end of destination path. + +Windows `subst` command require to have destination path with slash if it's a drive (it should be `G:\` not `G:`). + + +___ + +
+ + +
+Global: Move PyOpenColorIO to vendor/python #4946 + +So that DCCs don't conflict with their own. + +See https://github.com/ynput/OpenPype/pull/4267#issuecomment-1537153263 for the issue with Gaffer. + +I'm not sure if this is the correct approach, but I assume PySide/Shiboken is under `vendor/python` for this reason as well... +___ + +
+ + +
+RuntimeError with Click on deadline publish #5065 + +I changed Click to version 8.0 instead of 7.1.2 to solve this error: +``` +2023-05-30 16:16:51: 0: STDOUT: Traceback (most recent call last): +2023-05-30 16:16:51: 0: STDOUT: File "start.py", line 1126, in boot +2023-05-30 16:16:51: 0: STDOUT: File "/prod/softprod/apps/openpype/LINUX/3.15/dependencies/click/core.py", line 829, in __call__ +2023-05-30 16:16:51: 0: STDOUT: return self.main(*args, **kwargs) +2023-05-30 16:16:51: 0: STDOUT: File "/prod/softprod/apps/openpype/LINUX/3.15/dependencies/click/core.py", line 760, in main +2023-05-30 16:16:51: 0: STDOUT: _verify_python3_env() +2023-05-30 16:16:51: 0: STDOUT: File "/prod/softprod/apps/openpype/LINUX/3.15/dependencies/click/_unicodefun.py", line 126, in _verify_python3_env +2023-05-30 16:16:51: 0: STDOUT: raise RuntimeError( +2023-05-30 16:16:51: 0: STDOUT: RuntimeError: Click will abort further execution because Python 3 was configured to use ASCII as encoding for the environment. Consult https://click.palletsprojects.com/python3/ for mitigation steps. +``` + + +___ + +
+ + + + ## [3.15.12](https://github.com/ynput/OpenPype/tree/3.15.12) diff --git a/openpype/version.py b/openpype/version.py index 03f618ff75..219541620d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.0-nightly.1" +__version__ = "3.16.0" diff --git a/pyproject.toml b/pyproject.toml index 1da0880a67..fe5477fb00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.12" # OpenPype +version = "3.16.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 344672698137c20708235c7fe9ed5b6c21339897 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 13 Jul 2023 12:48:53 +0000 Subject: [PATCH 272/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 090e28a951..f402f4541d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.0 - 3.16.0-nightly.1 - 3.15.12 - 3.15.12-nightly.4 @@ -134,7 +135,6 @@ body: - 3.14.5 - 3.14.5-nightly.3 - 3.14.5-nightly.2 - - 3.14.5-nightly.1 validations: required: true - type: dropdown From 7bbea01593e93249ccf0571c4912ee4a4e827f94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jul 2023 15:13:54 +0200 Subject: [PATCH 273/446] fix: remove opentimelineio from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index f915f0e8ae..260728dde6 100644 --- a/setup.py +++ b/setup.py @@ -89,7 +89,6 @@ install_requires = [ "keyring", "clique", "jsonschema", - "opentimelineio", "pathlib2", "pkg_resources", "PIL", From c3e20024bf65a2bff967043635a96444a9726432 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 13 Jul 2023 15:55:39 +0200 Subject: [PATCH 274/446] Copy publish attributes from review instance to any attached instances --- openpype/hosts/maya/plugins/publish/collect_review.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 6cb10f9066..fa00fc661e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -107,6 +107,11 @@ class CollectReview(pyblish.api.InstancePlugin): data["displayLights"] = display_lights data["burninDataMembers"] = burninDataMembers + publish_attributes = data.setdefault("publish_attributes", {}) + for key, value in instance.data["publish_attributes"].items(): + if key not in publish_attributes: + publish_attributes[key] = value + # The review instance must be active cmds.setAttr(str(instance) + '.active', 1) From 8d9a283a0979400569baeee8e1589cf24345c992 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 13 Jul 2023 16:41:22 +0100 Subject: [PATCH 275/446] Include disabled plugins --- openpype/hosts/maya/plugins/create/convert_legacy.py | 5 ++--- openpype/pipeline/create/context.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/convert_legacy.py b/openpype/hosts/maya/plugins/create/convert_legacy.py index 6133abc205..33a1e020dd 100644 --- a/openpype/hosts/maya/plugins/create/convert_legacy.py +++ b/openpype/hosts/maya/plugins/create/convert_legacy.py @@ -51,7 +51,7 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, # From all current new style manual creators find the mapping # from family to identifier family_to_id = {} - for identifier, creator in self.create_context.manual_creators.items(): + for identifier, creator in self.create_context.creators.items(): family = getattr(creator, "family", None) if not family: continue @@ -70,7 +70,6 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, # logic was thus to be live to the current task to begin with. data = dict() data["task"] = self.create_context.get_current_task_name() - for family, instance_nodes in legacy.items(): if family not in family_to_id: self.log.warning( @@ -81,7 +80,7 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, continue creator_id = family_to_id[family] - creator = self.create_context.manual_creators[creator_id] + creator = self.create_context.creators[creator_id] data["creator_identifier"] = creator_id if isinstance(creator, plugin.RenderlayerCreator): diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 98fcee5fe5..614fd575b0 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1804,10 +1804,10 @@ class CreateContext: self, self.headless ) + creators[creator_identifier] = creator if not creator.enabled: disabled_creators[creator_identifier] = creator continue - creators[creator_identifier] = creator if isinstance(creator, AutoCreator): autocreators[creator_identifier] = creator elif isinstance(creator, Creator): From a80685c7f0713fb59e2737581b9e0864a7ea220a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 13 Jul 2023 16:43:57 +0100 Subject: [PATCH 276/446] Update the members of parent containers as well --- .../hosts/blender/plugins/load/load_blend.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index a0ccd65f29..ffd84a4b11 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -37,6 +37,17 @@ class BlendLoader(plugin.AssetLoader): return None + @staticmethod + def get_all_container_parents(asset_group): + parent_containers = [] + parent = asset_group.parent + while parent: + if parent.get(AVALON_PROPERTY): + parent_containers.append(parent) + parent = parent.parent + + return parent_containers + def _post_process_layout(self, container, asset, representation): rigs = [ obj for obj in container.children_recursive @@ -202,6 +213,13 @@ class BlendLoader(plugin.AssetLoader): imprint(asset_group, new_data) + # We need to update all the parent container members + parent_containers = self.get_all_container_parents(asset_group) + + for parent_container in parent_containers: + parent_members = parent_container[AVALON_PROPERTY]["members"] + parent_container[AVALON_PROPERTY]["members"] = parent_members + members + def exec_remove(self, container: Dict) -> bool: """ Remove an existing container from a Blender scene. @@ -217,9 +235,19 @@ class BlendLoader(plugin.AssetLoader): ) ] + members = asset_group.get(AVALON_PROPERTY).get("members", []) + + # We need to update all the parent container members + parent_containers = self.get_all_container_parents(asset_group) + + for parent in parent_containers: + parent.get(AVALON_PROPERTY)["members"] = list(filter( + lambda i: i not in members, + parent.get(AVALON_PROPERTY)["members"])) + for attr in attrs: for data in getattr(bpy.data, attr): - if data in asset_group.get(AVALON_PROPERTY).get("members", []): + if data in members: # Skip the asset group if data == asset_group: continue From 4faeea5b30a3eacf5040ec30e727f220e612f11b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 13 Jul 2023 16:53:09 +0100 Subject: [PATCH 277/446] Hound fixes --- openpype/hosts/blender/plugins/load/load_blend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index ffd84a4b11..db7663e0ed 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -218,7 +218,8 @@ class BlendLoader(plugin.AssetLoader): for parent_container in parent_containers: parent_members = parent_container[AVALON_PROPERTY]["members"] - parent_container[AVALON_PROPERTY]["members"] = parent_members + members + parent_container[AVALON_PROPERTY]["members"] = ( + parent_members + members) def exec_remove(self, container: Dict) -> bool: """ From 04af219c61627306b95992e18f8f414e0306554f Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 13 Jul 2023 17:12:47 +0100 Subject: [PATCH 278/446] Update openpype/hosts/maya/plugins/create/convert_legacy.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/create/convert_legacy.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/convert_legacy.py b/openpype/hosts/maya/plugins/create/convert_legacy.py index 33a1e020dd..2692215a38 100644 --- a/openpype/hosts/maya/plugins/create/convert_legacy.py +++ b/openpype/hosts/maya/plugins/create/convert_legacy.py @@ -51,7 +51,12 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, # From all current new style manual creators find the mapping # from family to identifier family_to_id = {} - for identifier, creator in self.create_context.creators.items(): + # Consider both disabled and enabled creators + # e.g. the "animation" creator is disabled to be hidden + # by the user + creators = self.create_context.disabled_creators.copy() + creators.update(self.create_context.creators.copy()) + for identifier, creator in creators.items(): family = getattr(creator, "family", None) if not family: continue From f560d6bcf9aede3a0cadcd7133779c3b3aff073f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 13 Jul 2023 18:37:11 +0200 Subject: [PATCH 279/446] moved collect frame range plugin to traypublisher plugins --- .../plugins/publish/collect_frame_range_asset_entity.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/{ => hosts/traypublisher}/plugins/publish/collect_frame_range_asset_entity.py (100%) diff --git a/openpype/plugins/publish/collect_frame_range_asset_entity.py b/openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py similarity index 100% rename from openpype/plugins/publish/collect_frame_range_asset_entity.py rename to openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py From bba1fa463442724935f914e33ff59c75cda0b581 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 13 Jul 2023 18:37:31 +0200 Subject: [PATCH 280/446] changed order so instances have filled 'assetEntity' --- .../plugins/publish/collect_frame_range_asset_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py b/openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py index ce744e2daf..c18e10e438 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py @@ -10,7 +10,7 @@ class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin, are not yet collected for the instance. """ - order = pyblish.api.CollectorOrder + 0.3 + order = pyblish.api.CollectorOrder + 0.491 label = "Collect Frame Data From Asset Entity" families = ["plate", "pointcache", "vdbcache", "online", From 3f10968c959998bf91e0af5204ca696c97d58dba Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 13 Jul 2023 18:06:35 +0100 Subject: [PATCH 281/446] Change solution for loading rig --- openpype/hosts/maya/api/lib.py | 9 +++------ openpype/hosts/maya/plugins/create/convert_legacy.py | 4 ++-- openpype/pipeline/create/context.py | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index cdc722a409..11d5ca1b41 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -27,20 +27,16 @@ from openpype.settings import get_project_settings from openpype.pipeline import ( get_current_project_name, get_current_asset_name, + get_current_task_name, discover_loader_plugins, loaders_from_representation, get_representation_path, load_container, - registered_host, + registered_host ) from openpype.lib import NumberDef from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline.create import CreateContext -from openpype.pipeline.context_tools import ( - get_current_asset_name, - get_current_project_name, - get_current_task_name -) from openpype.lib.profiles_filtering import filter_profiles @@ -4146,6 +4142,7 @@ def create_rig_animation_instance( host = registered_host() create_context = CreateContext(host) + create_context.creators.update(create_context.disabled_creators) # Create the animation instance with maintained_selection(): diff --git a/openpype/hosts/maya/plugins/create/convert_legacy.py b/openpype/hosts/maya/plugins/create/convert_legacy.py index 2692215a38..7e94d6f1e1 100644 --- a/openpype/hosts/maya/plugins/create/convert_legacy.py +++ b/openpype/hosts/maya/plugins/create/convert_legacy.py @@ -54,8 +54,8 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, # Consider both disabled and enabled creators # e.g. the "animation" creator is disabled to be hidden # by the user - creators = self.create_context.disabled_creators.copy() - creators.update(self.create_context.creators.copy()) + creators = self.create_context.creators.copy() + creators.update(self.create_context.disabled_creators.copy()) for identifier, creator in creators.items(): family = getattr(creator, "family", None) if not family: diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 614fd575b0..98fcee5fe5 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1804,10 +1804,10 @@ class CreateContext: self, self.headless ) - creators[creator_identifier] = creator if not creator.enabled: disabled_creators[creator_identifier] = creator continue + creators[creator_identifier] = creator if isinstance(creator, AutoCreator): autocreators[creator_identifier] = creator elif isinstance(creator, Creator): From d41f10e6f7a3920074bf812992302ad150e10785 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 13 Jul 2023 18:10:42 +0100 Subject: [PATCH 282/446] Fix converting --- openpype/hosts/maya/plugins/create/convert_legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/convert_legacy.py b/openpype/hosts/maya/plugins/create/convert_legacy.py index 7e94d6f1e1..61bc5a5e11 100644 --- a/openpype/hosts/maya/plugins/create/convert_legacy.py +++ b/openpype/hosts/maya/plugins/create/convert_legacy.py @@ -85,7 +85,7 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, continue creator_id = family_to_id[family] - creator = self.create_context.creators[creator_id] + creator = creators[creator_id] data["creator_identifier"] = creator_id if isinstance(creator, plugin.RenderlayerCreator): From c10ad8227aff1eb73dbd195bf6c1358b0c0cfa5d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 13 Jul 2023 18:11:40 +0100 Subject: [PATCH 283/446] Code cosmetics --- openpype/hosts/maya/plugins/create/convert_legacy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/convert_legacy.py b/openpype/hosts/maya/plugins/create/convert_legacy.py index 61bc5a5e11..302633b49d 100644 --- a/openpype/hosts/maya/plugins/create/convert_legacy.py +++ b/openpype/hosts/maya/plugins/create/convert_legacy.py @@ -51,9 +51,8 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, # From all current new style manual creators find the mapping # from family to identifier family_to_id = {} - # Consider both disabled and enabled creators - # e.g. the "animation" creator is disabled to be hidden - # by the user + # Consider both disabled and enabled creators e.g. the "animation" + # creator is disabled to be hidden from the user. creators = self.create_context.creators.copy() creators.update(self.create_context.disabled_creators.copy()) for identifier, creator in creators.items(): From 326cdacc115d9d8771eb537480375c6300936f7d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 14 Jul 2023 09:57:48 +0200 Subject: [PATCH 284/446] change 'ayon' to 'AYON' and 'ynput' to 'Ynput' in appdirs (#5298) --- common/ayon_common/utils.py | 2 +- openpype/lib/local_settings.py | 2 +- openpype/modules/base.py | 2 +- openpype/vendor/python/common/ayon_api/thumbnails.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/ayon_common/utils.py b/common/ayon_common/utils.py index d0638e552f..c0d0c7c0b1 100644 --- a/common/ayon_common/utils.py +++ b/common/ayon_common/utils.py @@ -16,7 +16,7 @@ def get_ayon_appdirs(*args): """ return os.path.join( - appdirs.user_data_dir("ayon", "ynput"), + appdirs.user_data_dir("AYON", "Ynput"), *args ) diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index 8f09c6be63..3fb35a7e7b 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -529,7 +529,7 @@ def get_ayon_appdirs(*args): """ return os.path.join( - appdirs.user_data_dir("ayon", "ynput"), + appdirs.user_data_dir("AYON", "Ynput"), *args ) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 24ddc97ac0..9b3637c48a 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -374,7 +374,7 @@ def _load_ayon_addons(openpype_modules, modules_key, log): if not addons_info: return v3_addons_to_skip addons_dir = os.path.join( - appdirs.user_data_dir("ayon", "ynput"), + appdirs.user_data_dir("AYON", "Ynput"), "addons" ) if not os.path.exists(addons_dir): diff --git a/openpype/vendor/python/common/ayon_api/thumbnails.py b/openpype/vendor/python/common/ayon_api/thumbnails.py index 50acd94dcb..11734ca762 100644 --- a/openpype/vendor/python/common/ayon_api/thumbnails.py +++ b/openpype/vendor/python/common/ayon_api/thumbnails.py @@ -50,7 +50,7 @@ class ThumbnailCache: """ if self._thumbnails_dir is None: - directory = appdirs.user_data_dir("ayon", "ynput") + directory = appdirs.user_data_dir("AYON", "Ynput") self._thumbnails_dir = os.path.join(directory, "thumbnails") return self._thumbnails_dir From d63c0123962008ffbd36ee0a6b38c7d6e32574b0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jul 2023 11:03:02 +0200 Subject: [PATCH 285/446] deadline removing OPENPYPE_VERSION from houdini and max submitters --- .../plugins/publish/submit_houdini_render_deadline.py | 1 - .../deadline/plugins/publish/submit_max_deadline.py | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 254914a850..af341ca8e8 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -88,7 +88,6 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "AVALON_APP_NAME", "OPENPYPE_DEV", "OPENPYPE_LOG_NO_COLORS", - "OPENPYPE_VERSION" ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index de3c7221d3..fff7a4ced5 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -20,6 +20,7 @@ from openpype.hosts.max.api.lib import ( from openpype.hosts.max.api.lib_rendersettings import RenderSettings from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo +from openpype.lib import is_running_from_build @attr.s @@ -110,9 +111,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "AVALON_TASK", "AVALON_APP_NAME", "OPENPYPE_DEV", - "OPENPYPE_VERSION", "IS_TEST" ] + + # Add OpenPype version if we are running from build. + if is_running_from_build(): + keys.append("OPENPYPE_VERSION") + # Add mongo url if it's enabled if self._instance.context.data.get("deadlinePassMongoUrl"): keys.append("OPENPYPE_MONGO") From 18324615d5c486c39d83e3f53da2172ec1cf6270 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Jul 2023 10:14:57 +0100 Subject: [PATCH 286/446] Fix parenting if creating an instance while selecting a whole hierarchy --- openpype/hosts/blender/plugins/create/create_rig.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 2e04fb71c1..08cc46ee3e 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -42,7 +42,9 @@ class CreateRig(plugin.Creator): bpy.context.view_layer.objects.active = asset_group selected = lib.get_selection() for obj in selected: - obj.select_set(True) + if obj.parent in selected: + obj.select_set(False) + continue selected.append(asset_group) bpy.ops.object.parent_set(keep_transform=True) From 167fd7186c02eaa4b88faceb12e6335878bc6994 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 14 Jul 2023 11:15:18 +0100 Subject: [PATCH 287/446] Hide CreateAnimation instead of disable --- openpype/hosts/maya/api/lib.py | 1 - .../hosts/maya/plugins/create/convert_legacy.py | 6 ++---- .../hosts/maya/plugins/create/create_animation.py | 15 +++++++-------- .../settings/defaults/project_settings/maya.json | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 11d5ca1b41..40b3419e73 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -4142,7 +4142,6 @@ def create_rig_animation_instance( host = registered_host() create_context = CreateContext(host) - create_context.creators.update(create_context.disabled_creators) # Create the animation instance with maintained_selection(): diff --git a/openpype/hosts/maya/plugins/create/convert_legacy.py b/openpype/hosts/maya/plugins/create/convert_legacy.py index 302633b49d..b02c863a43 100644 --- a/openpype/hosts/maya/plugins/create/convert_legacy.py +++ b/openpype/hosts/maya/plugins/create/convert_legacy.py @@ -53,9 +53,7 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, family_to_id = {} # Consider both disabled and enabled creators e.g. the "animation" # creator is disabled to be hidden from the user. - creators = self.create_context.creators.copy() - creators.update(self.create_context.disabled_creators.copy()) - for identifier, creator in creators.items(): + for identifier, creator in self.create_context.creators.items(): family = getattr(creator, "family", None) if not family: continue @@ -84,7 +82,7 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, continue creator_id = family_to_id[family] - creator = creators[creator_id] + creator = self.create_context.creators[creator_id] data["creator_identifier"] = creator_id if isinstance(creator, plugin.RenderlayerCreator): diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index cade8603ce..7482abefcc 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -6,17 +6,16 @@ from openpype.lib import ( BoolDef, TextDef ) +from openpype.pipeline.create import HiddenCreator -class CreateAnimation(plugin.MayaCreator): - """Animation output for character rigs""" - - # We hide the animation creator from the UI since the creation of it - # is automated upon loading a rig. There's an inventory action to recreate - # it for loaded rigs if by chance someone deleted the animation instance. - # Note: This setting is actually applied from project settings - enabled = False +class CreateAnimation(plugin.MayaCreator, HiddenCreator): + """Animation output for character rigs + We hide the animation creator from the UI since the creation of it is + automated upon loading a rig. There's an inventory action to recreate it + for loaded rigs if by chance someone deleted the animation instance. + """ identifier = "io.openpype.creators.maya.animation" name = "animationDefault" label = "Animation" diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index a25775e592..fe369b534e 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -555,7 +555,7 @@ "publish_mip_map": true }, "CreateAnimation": { - "enabled": false, + "enabled": true, "write_color_sets": false, "write_face_sets": false, "include_parent_hierarchy": false, From 5eb6b187f408cec8a1b910716a27c2dde5b4bca2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 14 Jul 2023 12:17:05 +0200 Subject: [PATCH 288/446] Removed loader settings for Harmony (#5289) It shouldn't be configurable, it is internal logic. By adding additional extension it wouldn't start to work magically. --- .../defaults/project_settings/harmony.json | 16 ----------- .../schema_project_harmony.json | 28 ------------------- 2 files changed, 44 deletions(-) diff --git a/openpype/settings/defaults/project_settings/harmony.json b/openpype/settings/defaults/project_settings/harmony.json index 02f51d1d2b..b424b43cc1 100644 --- a/openpype/settings/defaults/project_settings/harmony.json +++ b/openpype/settings/defaults/project_settings/harmony.json @@ -10,22 +10,6 @@ "rules": {} } }, - "load": { - "ImageSequenceLoader": { - "family": [ - "shot", - "render", - "image", - "plate", - "reference" - ], - "representations": [ - "jpeg", - "png", - "jpg" - ] - } - }, "publish": { "CollectPalettes": { "allowed_tasks": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index 98a815f2d4..f081c48b23 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -18,34 +18,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "load", - "label": "Loader plugins", - "children": [ - { - "type": "dict", - "collapsible": true, - "key": "ImageSequenceLoader", - "label": "Load Image Sequence", - "children": [ - { - "type": "list", - "key": "family", - "label": "Families", - "object_type": "text" - }, - { - "type": "list", - "key": "representations", - "label": "Representations", - "object_type": "text" - } - ] - } - ] - }, { "type": "dict", "collapsible": true, From 97d8f89e44fc40663388d0af69d2f7043a92a430 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 14 Jul 2023 11:17:37 +0100 Subject: [PATCH 289/446] Code cosmetics --- openpype/hosts/maya/plugins/create/convert_legacy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/convert_legacy.py b/openpype/hosts/maya/plugins/create/convert_legacy.py index b02c863a43..33a1e020dd 100644 --- a/openpype/hosts/maya/plugins/create/convert_legacy.py +++ b/openpype/hosts/maya/plugins/create/convert_legacy.py @@ -51,8 +51,6 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, # From all current new style manual creators find the mapping # from family to identifier family_to_id = {} - # Consider both disabled and enabled creators e.g. the "animation" - # creator is disabled to be hidden from the user. for identifier, creator in self.create_context.creators.items(): family = getattr(creator, "family", None) if not family: From 585df831357dc859947b294d1a9af2c4a73b76c5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Jul 2023 11:28:36 +0100 Subject: [PATCH 290/446] Apply parenting fix to other families as well --- openpype/hosts/blender/plugins/create/create_camera.py | 4 +++- openpype/hosts/blender/plugins/create/create_layout.py | 4 +++- openpype/hosts/blender/plugins/create/create_model.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 6defe02fe5..7a770a3e77 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -43,7 +43,9 @@ class CreateCamera(plugin.Creator): bpy.context.view_layer.objects.active = asset_group selected = lib.get_selection() for obj in selected: - obj.select_set(True) + if obj.parent in selected: + obj.select_set(False) + continue selected.append(asset_group) bpy.ops.object.parent_set(keep_transform=True) else: diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 68cfaa41ac..73ed683256 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -42,7 +42,9 @@ class CreateLayout(plugin.Creator): bpy.context.view_layer.objects.active = asset_group selected = lib.get_selection() for obj in selected: - obj.select_set(True) + if obj.parent in selected: + obj.select_set(False) + continue selected.append(asset_group) bpy.ops.object.parent_set(keep_transform=True) diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index e5204b5b53..51fc6683f6 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -42,7 +42,9 @@ class CreateModel(plugin.Creator): bpy.context.view_layer.objects.active = asset_group selected = lib.get_selection() for obj in selected: - obj.select_set(True) + if obj.parent in selected: + obj.select_set(False) + continue selected.append(asset_group) bpy.ops.object.parent_set(keep_transform=True) From 16a9e707d8fa4785df6931142a2bebcee332031a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 14 Jul 2023 11:34:42 +0100 Subject: [PATCH 291/446] Apply project settings to creators --- openpype/hosts/maya/api/plugin.py | 20 +++++++++++++++++++ .../create/create_arnold_scene_source.py | 1 + 2 files changed, 21 insertions(+) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 2b5aee9700..40b2374073 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -181,6 +181,8 @@ class MayaCreatorBase(object): @six.add_metaclass(ABCMeta) class MayaCreator(NewCreator, MayaCreatorBase): + settings_name = None + def create(self, subset_name, instance_data, pre_create_data): members = list() @@ -238,6 +240,24 @@ class MayaCreator(NewCreator, MayaCreatorBase): default=True) ] + def apply_settings(self, project_settings, system_settings): + """Method called on initialization of plugin to apply settings.""" + + settings_name = self.settings_name + if settings_name is None: + settings_name = self.__class__.__name__ + + settings = project_settings["maya"]["create"] + settings = settings.get(settings_name) + if settings is None: + self.log.debug( + "No settings found for {}".format(self.__class__.__name__) + ) + return + + for key, value in settings.items(): + setattr(self, key, value) + def ensure_namespace(namespace): """Make sure the namespace exists. diff --git a/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py b/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py index 0c8cf8d2bb..1ef132725f 100644 --- a/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py @@ -15,6 +15,7 @@ class CreateArnoldSceneSource(plugin.MayaCreator): label = "Arnold Scene Source" family = "ass" icon = "cube" + settings_name = "CreateAss" expandProcedurals = False motionBlur = True From aaafb9ccf2a271aa7571df7be15baceffe6e48c9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Jul 2023 12:05:49 +0100 Subject: [PATCH 292/446] Added blendScene family --- .../plugins/create/create_blendScene.py | 51 +++++++++++++++++++ .../hosts/blender/plugins/load/load_blend.py | 2 +- .../blender/plugins/publish/extract_blend.py | 2 +- openpype/plugins/publish/integrate.py | 3 +- .../defaults/project_settings/blender.json | 3 +- 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/blender/plugins/create/create_blendScene.py diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py new file mode 100644 index 0000000000..63bcf212ff --- /dev/null +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -0,0 +1,51 @@ +"""Create a Blender scene asset.""" + +import bpy + +from openpype.pipeline import get_current_task_name +from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES + + +class CreateBlendScene(plugin.Creator): + """Generic group of assets""" + + name = "blendScene" + label = "Blender Scene" + family = "blendScene" + icon = "cubes" + + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process) + ops.execute_in_main_thread(mti) + + def _process(self): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + asset = self.data["asset"] + subset = self.data["subset"] + name = plugin.asset_name(asset, subset) + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) + self.data['task'] = get_current_task_name() + lib.imprint(asset_group, self.data) + + # Add selected objects to instance + if (self.options or {}).get("useSelection"): + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + if obj.parent in selected: + obj.select_set(False) + continue + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + + return asset_group diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index db7663e0ed..99f291a5a7 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -20,7 +20,7 @@ from openpype.hosts.blender.api.pipeline import ( class BlendLoader(plugin.AssetLoader): """Load assets from a .blend file.""" - families = ["model", "rig", "layout", "camera"] + families = ["model", "rig", "layout", "camera", "blendScene"] representations = ["blend"] label = "Append Blend" diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index 6a001b6f65..d4f26b4f3c 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -10,7 +10,7 @@ class ExtractBlend(publish.Extractor): label = "Extract Blend" hosts = ["blender"] - families = ["model", "camera", "rig", "action", "layout"] + families = ["model", "camera", "rig", "action", "layout", "blendScene"] optional = True def process(self, instance): diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index e76f9ce9c4..ffb9acf4a7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -137,7 +137,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "mvUsdOverride", "simpleUnrealTexture", "online", - "uasset" + "uasset", + "blendScene" ] default_template_name = "publish" diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index eae5b239c8..29e61fe233 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -54,7 +54,8 @@ "camera", "rig", "action", - "layout" + "layout", + "blendScene" ] }, "ExtractFBX": { From 103c8cd56e580bafd4f59813d0086153a487fbef Mon Sep 17 00:00:00 2001 From: Jiri Sindelar Date: Fri, 14 Jul 2023 13:37:28 +0200 Subject: [PATCH 293/446] Read pixel aspect from input --- .../plugins/publish/extract_slate_frame.py | 2 +- .../plugins/publish/extract_review_slate.py | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index 06c086b10d..54c88717c5 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -242,7 +242,7 @@ class ExtractSlateFrame(publish.Extractor): # render slate as sequence frame nuke.execute( - instance.data["name"], + str(instance.data["name"]), int(slate_first_frame), int(slate_first_frame) ) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index fca3d96ca6..75c501a85c 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -86,8 +86,11 @@ class ExtractReviewSlate(publish.Extractor): input_width, input_height, input_timecode, - input_frame_rate + input_frame_rate, + input_pixel_aspect ) = self._get_video_metadata(streams) + if input_pixel_aspect: + pixel_aspect = input_pixel_aspect # Raise exception of any stream didn't define input resolution if input_width is None: @@ -421,6 +424,7 @@ class ExtractReviewSlate(publish.Extractor): input_width = None input_height = None input_frame_rate = None + input_pixel_aspect = None for stream in streams: if stream.get("codec_type") != "video": continue @@ -438,6 +442,16 @@ class ExtractReviewSlate(publish.Extractor): input_width = width input_height = height + input_pixel_aspect = str(stream.get("sample_aspect_ratio")) + if input_pixel_aspect is not None: + try: + input_pixel_aspect = float( + eval(input_pixel_aspect.replace(':', '/'))) + except Exception: + self.log.debug( + "__Converting pixel aspect to float failed: {}".format( + input_pixel_aspect)) + tags = stream.get("tags") or {} input_timecode = tags.get("timecode") or "" @@ -448,7 +462,8 @@ class ExtractReviewSlate(publish.Extractor): input_width, input_height, input_timecode, - input_frame_rate + input_frame_rate, + input_pixel_aspect ) def _get_audio_metadata(self, streams): From 8cb07fceabab9a9df90fa6a82854fe4992717c4c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jul 2023 15:29:19 +0200 Subject: [PATCH 294/446] removing unused option for workflow --- .github/workflows/miletone_release_trigger.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/miletone_release_trigger.yml b/.github/workflows/miletone_release_trigger.yml index 4a031be7f9..d755f7eb9f 100644 --- a/.github/workflows/miletone_release_trigger.yml +++ b/.github/workflows/miletone_release_trigger.yml @@ -5,12 +5,6 @@ on: inputs: milestone: required: true - release-type: - type: choice - description: What release should be created - options: - - release - - pre-release milestone: types: closed From 916e9cfa974c79f53e161f5d3c681855b42e210f Mon Sep 17 00:00:00 2001 From: Jiri Sindelar Date: Fri, 14 Jul 2023 15:41:39 +0200 Subject: [PATCH 295/446] Allow exporting with no timecode knob --- openpype/hosts/nuke/api/plugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 7035da2bb5..4755fa8c56 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -955,7 +955,11 @@ class ExporterReviewMov(ExporterReview): except Exception: self.log.info("`mov64_codec` knob was not found") - write_node["mov64_write_timecode"].setValue(1) + try: + write_node["mov64_write_timecode"].setValue(1) + except Exception: + self.log.info("`mov64_write_timecode` knob was not found") + write_node["raw"].setValue(1) # connect write_node.setInput(0, self.previous_node) From 266f2308efba8bc6aa892cb473448d085fd922ac Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Jul 2023 15:46:46 +0100 Subject: [PATCH 296/446] Increment workfile version when publishing blendScene --- .../hosts/blender/plugins/publish/increment_workfile_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 963ca1398f..27fa4baf28 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -9,7 +9,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin): label = "Increment Workfile Version" optional = True hosts = ["blender"] - families = ["animation", "model", "rig", "action", "layout"] + families = ["animation", "model", "rig", "action", "layout", "blendScene"] def process(self, context): From b8f35bb33c642d0cc71b645e16f54b9f9cb8a65a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 15 Jul 2023 03:31:54 +0000 Subject: [PATCH 297/446] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 219541620d..2d396e5d30 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.0" +__version__ = "3.16.0-nightly.2" From 9d702a93d2c48840123140fbe01653aa62fe2379 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 15 Jul 2023 03:32:33 +0000 Subject: [PATCH 298/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f402f4541d..c0f74b5bc5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -36,6 +36,7 @@ body: description: What version are you running? Look to OpenPype Tray options: - 3.16.0 + - 3.16.0-nightly.2 - 3.16.0-nightly.1 - 3.15.12 - 3.15.12-nightly.4 @@ -134,7 +135,6 @@ body: - 3.14.6-nightly.1 - 3.14.5 - 3.14.5-nightly.3 - - 3.14.5-nightly.2 validations: required: true - type: dropdown From afa1d3cb031b536976c0b9a98d4512d03e1db21a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Jul 2023 13:06:59 +0200 Subject: [PATCH 299/446] fix args for workfile conversion util (#5308) --- openpype/client/server/conversion_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index dc95bbeda5..24d4678095 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -1320,7 +1320,9 @@ def convert_update_representation_to_v4( return flat_data -def convert_update_workfile_info_to_v4(update_data): +def convert_update_workfile_info_to_v4( + project_name, workfile_id, update_data, con +): return { key: value for key, value in update_data.items() From 2be5b33510a869d560bb95e0fb3e33ed93400f03 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 17 Jul 2023 13:14:18 +0200 Subject: [PATCH 300/446] Fix wrong merge --- .../plugins/publish/validate_primitive_hierarchy_paths.py | 8 -------- openpype/hosts/max/api/plugin.py | 2 -- 2 files changed, 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index 3da5665f58..0d84aa7db8 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -70,14 +70,6 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): cls.log.debug("Checking for attribute: %s", path_attr) - if not hasattr(output_node, "geometry"): - # In the case someone has explicitly set an Object - # node instead of a SOP node in Geometry context - # then for now we ignore - this allows us to also - # export object transforms. - cls.log.warning("No geometry output node found, skipping check..") - return - if not hasattr(output_node, "geometry"): # In the case someone has explicitly set an Object # node instead of a SOP node in Geometry context diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 36b4ea32d4..d8db716e6d 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -78,9 +78,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" if idx do ( continue ) - name = c as string - append temp_arr handle_name append i_node_arr node_ref append sel_list name From b90d1c9e653cf9badfbce7de7f5442f55cb6eeef Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 17 Jul 2023 13:22:04 +0200 Subject: [PATCH 301/446] Fix wrong merge --- openpype/hosts/maya/api/fbx.py | 4 ++-- openpype/settings/defaults/project_settings/shotgrid.json | 2 +- poetry.lock | 2 +- pyproject.toml | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index e9cf8af491..260241f5fc 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -2,7 +2,7 @@ """Tools to work with FBX.""" import logging -import pyblish.api +from pyblish.api import Instance from maya import cmds # noqa import maya.mel as mel # noqa @@ -141,7 +141,7 @@ class FBXExtractor: return options def set_options_from_instance(self, instance): - # type: (pyblish.api.Instance) -> None + # type: (Instance) -> None """Sets FBX export options from data in the instance. Args: diff --git a/openpype/settings/defaults/project_settings/shotgrid.json b/openpype/settings/defaults/project_settings/shotgrid.json index 0dcffed28a..83b6f69074 100644 --- a/openpype/settings/defaults/project_settings/shotgrid.json +++ b/openpype/settings/defaults/project_settings/shotgrid.json @@ -1,6 +1,6 @@ { "shotgrid_project_id": 0, - "shotgrid_server": [], + "shotgrid_server": "", "event": { "enabled": false }, diff --git a/poetry.lock b/poetry.lock index 50f8150638..5621d39988 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "acre" diff --git a/pyproject.toml b/pyproject.toml index 5b58257310..fe5477fb00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,6 @@ wheel = "*" enlighten = "*" # cool terminal progress bars toml = "^0.10.2" # for parsing pyproject.toml pre-commit = "*" -mypy = "*" # for better types [tool.poetry.urls] "Bug Tracker" = "https://github.com/pypeclub/openpype/issues" From 363af956d48ea7509b87dffdf0a056d924249d90 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 17 Jul 2023 15:32:01 +0200 Subject: [PATCH 302/446] replace endswith by startswith in rig outputs id need this one for publish multiple rig in one asset. --- openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index 75447fdfea..841d005178 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -47,7 +47,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): invalid = {} if compute: - out_set = next(x for x in instance if x.endswith("out_SET")) + out_set = next(x for x in instance if x.startswith("out_SET")) instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True) instance_nodes = cmds.ls(instance_nodes, long=True) From 0d0681f621281bd38920d64d018a7ac06a3dc70c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 17 Jul 2023 15:45:17 +0100 Subject: [PATCH 303/446] Hardcode enabled state --- .../hosts/maya/plugins/create/create_animation.py | 12 +++++++++--- .../projects_schema/schemas/schema_maya_create.json | 6 ++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index 7482abefcc..7424d1c590 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -27,9 +27,6 @@ class CreateAnimation(plugin.MayaCreator, HiddenCreator): include_parent_hierarchy = False include_user_defined_attributes = False - # TODO: Would be great if we could visually hide this from the creator - # by default but do allow to generate it through code. - def get_instance_attr_defs(self): defs = lib.collect_animation_defs() @@ -84,3 +81,12 @@ class CreateAnimation(plugin.MayaCreator, HiddenCreator): """ return defs + + def apply_settings(self, project_settings, system_settings): + super(CreateAnimation, self).apply_settings( + project_settings, system_settings + ) + # Hardcoding creator to be enabled due to existing settings would + # disable the creator causing the creator plugin to not be + # discoverable. + self.enabled = True diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 1c37638c90..d28d42c10c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -120,12 +120,10 @@ "collapsible": true, "key": "CreateAnimation", "label": "Create Animation", - "checkbox_key": "enabled", "children": [ { - "type": "boolean", - "key": "enabled", - "label": "Enabled" + "type": "label", + "label": "This plugin is not optional due to implicit creation through loading the \"rig\" family.\nThis family is also hidden from creation due to complexity in setup." }, { "type": "boolean", From 3e5aa1033b654e87b47ed52f1663ef31bc7edc67 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 17 Jul 2023 17:19:51 +0200 Subject: [PATCH 304/446] OP-4845 - created new AYON_* env var to differentiate Deadline jobs OP and Ayon will live together for a while so jobs sent to DL need to be differentiated by new env vars. --- .../modules/deadline/abstract_submit_deadline.py | 9 +++++++++ .../plugins/publish/submit_aftereffects_deadline.py | 4 ++-- .../plugins/publish/submit_harmony_deadline.py | 4 ++-- .../publish/submit_houdini_render_deadline.py | 4 ++-- .../deadline/plugins/publish/submit_max_deadline.py | 4 ++-- .../deadline/plugins/publish/submit_maya_deadline.py | 4 ++-- .../publish/submit_maya_remote_publish_deadline.py | 7 ++++++- .../deadline/plugins/publish/submit_nuke_deadline.py | 7 +++++-- .../deadline/plugins/publish/submit_publish_job.py | 12 +++++++++--- .../repository/custom/plugins/GlobalJobPreLoad.py | 2 +- 10 files changed, 40 insertions(+), 17 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 551a2f7373..85b537360c 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -394,6 +394,15 @@ class DeadlineJobInfo(object): for key, value in data.items(): setattr(self, key, value) + def add_render_job_env_var(self): + """Check if in OP or AYON mode and use appropriate env var.""" + render_job = ( + "AYON_RENDER_JOB" if os.environ.get("USE_AYON_SERVER") == '1' + else "OPENPYPE_RENDER_JOB") + + self.EnvironmentKeyValue[render_job] = "1" + + @six.add_metaclass(AbstractMetaInstancePlugin) class AbstractSubmitDeadline(pyblish.api.InstancePlugin, diff --git a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py index 83dd5b49e2..009375e87e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -106,8 +106,8 @@ class AfterEffectsSubmitDeadline( if value: dln_job_info.EnvironmentKeyValue[key] = value - # to recognize job from PYPE for turning Event On/Off - dln_job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" + # to recognize render jobs + dln_job_info.add_render_job_env_var() return dln_job_info diff --git a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py index 84fca11d9d..2c37268f04 100644 --- a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -299,8 +299,8 @@ class HarmonySubmitDeadline( if value: job_info.EnvironmentKeyValue[key] = value - # to recognize job from PYPE for turning Event On/Off - job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" + # to recognize render jobs + job_info.add_render_job_env_var() return job_info diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index af341ca8e8..8c814bec95 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -105,8 +105,8 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): if value: job_info.EnvironmentKeyValue[key] = value - # to recognize job from PYPE for turning Event On/Off - job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" + # to recognize render jobs + job_info.add_render_job_env_var(job_info) for i, filepath in enumerate(instance.data["files"]): dirname = os.path.dirname(filepath) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index fff7a4ced5..2c1db1c880 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -131,8 +131,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, continue job_info.EnvironmentKeyValue[key] = value - # to recognize job from PYPE for turning Event On/Off - job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" + # to recognize render jobs + job_info.add_render_job_env_var(job_info) job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1" # Add list of expected files to job diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 159ac43289..d14daf0823 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -225,8 +225,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, continue job_info.EnvironmentKeyValue[key] = value - # to recognize job from PYPE for turning Event On/Off - job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" + # to recognize render jobs + job_info.add_render_job_env_var() job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1" # Adding file dependencies. diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 39120f7c8a..d7440fd0f4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -114,11 +114,16 @@ class MayaSubmitRemotePublishDeadline( environment["AVALON_TASK"] = instance.context.data["task"] environment["AVALON_APP_NAME"] = os.environ.get("AVALON_APP_NAME") environment["OPENPYPE_LOG_NO_COLORS"] = "1" - environment["OPENPYPE_REMOTE_JOB"] = "1" environment["OPENPYPE_USERNAME"] = instance.context.data["user"] environment["OPENPYPE_PUBLISH_SUBSET"] = instance.data["subset"] environment["OPENPYPE_REMOTE_PUBLISH"] = "1" + if os.environ.get("USE_AYON_SERVER") == '1': + environment["AYON_REMOTE_PUBLISH"] = "1" + else: + environment["OPENPYPE_REMOTE_PUBLISH"] = "1" + + for key, value in environment.items(): job_info.EnvironmentKeyValue[key] = value diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 4900231783..8f68a3a480 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -337,8 +337,11 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, if _path.lower().startswith('openpype_'): environment[_path] = os.environ[_path] - # to recognize job from PYPE for turning Event On/Off - environment["OPENPYPE_RENDER_JOB"] = "1" + # to recognize render jobs + render_job_label = ( + "AYON_RENDER_JOB" if os.environ.get("USE_AYON_SERVER") == '1' + else "OPENPYPE_RENDER_JOB") + environment[render_job_label] = "1" # finally search replace in values of any key if self.env_search_replace_values: diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 01a5c55286..161cf25cde 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -255,13 +255,19 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "AVALON_ASSET": instance.context.data["asset"], "AVALON_TASK": instance.context.data["task"], "OPENPYPE_USERNAME": instance.context.data["user"], - "OPENPYPE_PUBLISH_JOB": "1", - "OPENPYPE_RENDER_JOB": "0", - "OPENPYPE_REMOTE_JOB": "0", "OPENPYPE_LOG_NO_COLORS": "1", "IS_TEST": str(int(is_in_tests())) } + if os.environ.get("USE_AYON_SERVER") == '1': + environment["AYON_PUBLISH_JOB"] = "1" + environment["AYON_RENDER_JOB"] = "0" + environment["AYON_REMOTE_PUBLISH"] = "0" + else: + environment["OPENPYPE_PUBLISH_JOB"] = "1" + environment["OPENPYPE_RENDER_JOB"] = "0" + environment["OPENPYPE_REMOTE_PUBLISH"] = "0" + # add environments from self.environ_keys for env_key in self.environ_keys: if os.getenv(env_key): diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 15226bb773..d69aa12b5a 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -422,7 +422,7 @@ def __main__(deadlinePlugin): openpype_publish_job = \ job.GetJobEnvironmentKeyValue('OPENPYPE_PUBLISH_JOB') or '0' openpype_remote_job = \ - job.GetJobEnvironmentKeyValue('OPENPYPE_REMOTE_JOB') or '0' + job.GetJobEnvironmentKeyValue('OPENPYPE_REMOTE_PUBLISH') or '0' print("--- Job type - render {}".format(openpype_render_job)) print("--- Job type - publish {}".format(openpype_publish_job)) From a40e64ee0bb406fc4d6e0184da7e830d98ea3dd7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 17 Jul 2023 17:20:38 +0200 Subject: [PATCH 305/446] OP-4845 - added Ayon DL plugin --- .../repository/custom/plugins/Ayon/Ayon.ico | Bin 0 -> 7679 bytes .../custom/plugins/Ayon/Ayon.options | 9 + .../repository/custom/plugins/Ayon/Ayon.param | 17 ++ .../repository/custom/plugins/Ayon/Ayon.py | 235 ++++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.ico create mode 100644 openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.options create mode 100644 openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.param create mode 100644 openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.ico b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.ico new file mode 100644 index 0000000000000000000000000000000000000000..aea977a1251232d0f3d78ea6cb124994f4d31eb4 GIT binary patch literal 7679 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 dg=}F#T++JjmkP-0+ukjp@", str(self.GetStartFrame()), + arguments) + arguments = re.sub(r"<(?i)ENDFRAME>", str(self.GetEndFrame()), + arguments) + arguments = re.sub(r"<(?i)QUOTE>", "\"", arguments) + + arguments = self.ReplacePaddedFrame(arguments, + "<(?i)STARTFRAME%([0-9]+)>", + self.GetStartFrame()) + arguments = self.ReplacePaddedFrame(arguments, + "<(?i)ENDFRAME%([0-9]+)>", + self.GetEndFrame()) + + count = 0 + for filename in self.GetAuxiliaryFilenames(): + localAuxFile = Path.Combine(self.GetJobsDataDirectory(), filename) + arguments = re.sub(r"<(?i)AUXFILE" + str(count) + r">", + localAuxFile.replace("\\", "/"), arguments) + count += 1 + + return arguments + + def ReplacePaddedFrame(self, arguments, pattern, frame): + frameRegex = Regex(pattern) + while True: + frameMatch = frameRegex.Match(arguments) + if frameMatch.Success: + paddingSize = int(frameMatch.Groups[1].Value) + if paddingSize > 0: + padding = StringUtils.ToZeroPaddedString(frame, + paddingSize, + False) + else: + padding = str(frame) + arguments = arguments.replace(frameMatch.Groups[0].Value, + padding) + else: + break + + return arguments + + def HandleProgress(self): + progress = float(self.GetRegexMatch(1)) + self.SetProgress(progress) From 375e066ee00cf394ff4a3f4273657bab62f245a1 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 17 Jul 2023 15:21:39 +0000 Subject: [PATCH 306/446] [Automated] Release --- CHANGELOG.md | 150 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 152 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33dbdb14fa..07b95c7343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,156 @@ # Changelog +## [3.16.1](https://github.com/ynput/OpenPype/tree/3.16.1) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.0...3.16.1) + +### **🆕 New features** + + +
+Royal Render: Maya and Nuke support #5191 + +Basic working implementation of Royal Render support in Maya.It expects New publisher implemented in Maya. + + +___ + +
+ + +
+Blender: Blend File Family #4321 + +Implementation of the Blend File family analogue to the Maya Scene one. + + +___ + +
+ + +
+Houdini: simple bgeo publishing #4588 + +Support for simple publishing of bgeo files. + +This is adding basic support for bgeo publishing in Houdini. It will allow publishing bgeo in all supported formats (selectable in the creator options). If selected node has `output` on sop level, it will be used automatically as path in file node. + + +___ + +
+ +### **🚀 Enhancements** + + +
+General: delivery action add renamed frame number in Loader #5024 + +Frame Offset options for delivery in Openpype loader + + +___ + +
+ + +
+Enhancement/houdini add path action for abc validator #5237 + +Add a default path attribute Action.it's a helper action more than a repair action, which used to add a default single value. + + +___ + +
+ + +
+Nuke: auto apply all settings after template build #5277 + +Adding auto run of Apply All Settings after template is builder is finishing its process. This will apply Frame-range, Image size, Colorspace found in context of a task shot. + + +___ + +
+ + +
+Harmony:Removed loader settings for Harmony #5289 + +It shouldn't be configurable, it is internal logic. By adding additional extension it wouldn't start to work magically. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+AYON: Make appdirs case sensitive #5298 + +Appdirs for AYON are case sensitive for linux and mac so we needed to change them to match ayon launcher. Changed 'ayon' to 'AYON' and 'ynput' to 'Ynput'. + + +___ + +
+ + +
+Traypublisher: Fix plugin order #5299 + +Frame range collector for traypublisher was moved to traypublisher plugins and changed order to make sure `assetEntity` is filled in `instance.data`. + + +___ + +
+ + +
+Deadline: removing OPENPYPE_VERSION from some host submitters #5302 + +Removing deprecated method of adding OPENPYPE_VERSION to job environment. It was leftover and other hosts have already been cleared. + + +___ + +
+ + +
+AYON: Fix args for workfile conversion util #5308 + +Workfile update conversion util function have right expected arguments. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Maya: Refactor imports to `lib.get_reference_node` since the other function… #5258 + +Refactor imports to `lib.get_reference_node` since the other function is deprecated. + + +___ + +
+ + + + ## [3.16.0](https://github.com/ynput/OpenPype/tree/3.16.0) diff --git a/openpype/version.py b/openpype/version.py index 2d396e5d30..b2dfc857a3 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.0-nightly.2" +__version__ = "3.16.1" diff --git a/pyproject.toml b/pyproject.toml index fe5477fb00..fb6e222f27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.0" # OpenPype +version = "3.16.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 578121652ff419994200a74116b9c38efbab4a95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Jul 2023 15:22:44 +0000 Subject: [PATCH 307/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a2ca0d5e48..31d1ec74f5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.1 - 3.16.0 - 3.16.0-nightly.2 - 3.16.0-nightly.1 @@ -134,12 +135,6 @@ body: - 3.14.6-nightly.2 - 3.14.6-nightly.1 - 3.14.5 - - 3.14.5-nightly.3 - - 3.14.5-nightly.2 - - 3.14.5-nightly.1 - - 3.14.4 - - 3.14.4-nightly.4 - validations: required: true - type: dropdown From 55c3ef7a68688447671596d1acc3f4fbaf6db21b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Jul 2023 17:50:42 +0200 Subject: [PATCH 308/446] define 'multiselection' for shotgrid url to singleselection --- .../schemas/projects_schema/schema_project_shotgrid.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_shotgrid.json b/openpype/settings/entities/schemas/projects_schema/schema_project_shotgrid.json index 4faeca89f3..a5f1c57121 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_shotgrid.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_shotgrid.json @@ -13,7 +13,8 @@ { "type": "shotgrid_url-enum", "key": "shotgrid_server", - "label": "Shotgrid Server" + "label": "Shotgrid Server", + "multiselection": false }, { "type": "dict", From b915a60ccd4723cd17c7f52f5cd62d98e073e564 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Jul 2023 17:50:56 +0200 Subject: [PATCH 309/446] rename 'FarmRootEnumEntity' to 'DynamicEnumEntity' --- openpype/settings/entities/enum_entity.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 36deb3176e..26ecd33551 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -479,8 +479,7 @@ class TaskTypeEnumEntity(BaseEnumEntity): self.set(value_on_not_set) -@six.add_metaclass(abc.ABCMeta) -class FarmRootEnumEntity(BaseEnumEntity): +class DynamicEnumEntity(BaseEnumEntity): schema_types = [] def _item_initialization(self): @@ -500,7 +499,7 @@ class FarmRootEnumEntity(BaseEnumEntity): self.placeholder = self.schema_data.get("placeholder") def set_override_state(self, *args, **kwargs): - super(FarmRootEnumEntity, self).set_override_state(*args, **kwargs) + super(DynamicEnumEntity, self).set_override_state(*args, **kwargs) self.enum_items, self.valid_keys = self._get_enum_values() if self.multiselection: @@ -522,7 +521,7 @@ class FarmRootEnumEntity(BaseEnumEntity): pass -class DeadlineUrlEnumEntity(FarmRootEnumEntity): +class DeadlineUrlEnumEntity(DynamicEnumEntity): schema_types = ["deadline_url-enum"] def _get_enum_values(self): @@ -540,7 +539,7 @@ class DeadlineUrlEnumEntity(FarmRootEnumEntity): return enum_items_list, valid_keys -class RoyalRenderRootEnumEntity(FarmRootEnumEntity): +class RoyalRenderRootEnumEntity(DynamicEnumEntity): schema_types = ["rr_root-enum"] def _get_enum_values(self): @@ -558,7 +557,7 @@ class RoyalRenderRootEnumEntity(FarmRootEnumEntity): return enum_items_list, valid_keys -class ShotgridUrlEnumEntity(FarmRootEnumEntity): +class ShotgridUrlEnumEntity(DynamicEnumEntity): schema_types = ["shotgrid_url-enum"] def _get_enum_values(self): From 39ea6b102b96b6f86a147339986800cd3f175cb2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 17 Jul 2023 18:18:32 +0200 Subject: [PATCH 310/446] Fix missing context argument --- .../deadline/plugins/publish/submit_publish_job.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 0529fb8a70..2ed21c0621 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -188,6 +188,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, deepcopy(instance.data["anatomyData"]), instance.data.get("asset"), instances[0]["subset"], + instance.context, 'render', override_version ) @@ -523,8 +524,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, json.dump(publish_job, f, indent=4, sort_keys=True) def _get_publish_folder(self, anatomy, template_data, - asset, subset, - family='render', version=None): + asset, subset, context, + family, version=None): """ Extracted logic to pre-calculate real publish folder, which is calculated in IntegrateNew inside of Deadline process. @@ -550,7 +551,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, based on 'publish' template """ - project_name = self.context.data["projectName"] + project_name = context.data["projectName"] if not version: version = get_last_version_by_subset_name( project_name, @@ -563,7 +564,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, version = 1 template_data["subset"] = subset - template_data["family"] = "render" + template_data["family"] = family template_data["version"] = version render_templates = anatomy.templates_obj["render"] From 6f1400433da3c07b9d27fd0f19e7c8dd96346045 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 17 Jul 2023 18:40:31 +0200 Subject: [PATCH 311/446] Replace '-' with none Validator would complain about non-existent pool --- .../modules/deadline/plugins/publish/collect_pools.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index e221eb00ea..4196acfed2 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -36,12 +36,20 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, instance.data["primaryPool"] = ( attr_values.get("primaryPool") or self.primary_pool or "none" ) + if instance.data["primaryPool"] == "-": + instance.data["primaryPool"] = None if not instance.data.get("secondaryPool"): instance.data["secondaryPool"] = ( attr_values.get("secondaryPool") or self.secondary_pool or "none" # noqa ) + if instance.data["secondaryPool"] == "-": + instance.data["secondaryPool"] = None + + self.log.info("prima::{}".format(instance.data["primaryPool"])) + self.log.info("secondaryPool::{}".format(instance.data["secondaryPool"])) + @classmethod def get_attribute_defs(cls): # TODO: Preferably this would be an enum for the user From 7c0466df3241c328ddfddefb3f0b5d8fbaf916a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:49:31 +0200 Subject: [PATCH 312/446] Formatting fix --- openpype/modules/deadline/plugins/publish/collect_pools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index 4196acfed2..706374d972 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -48,7 +48,8 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, instance.data["secondaryPool"] = None self.log.info("prima::{}".format(instance.data["primaryPool"])) - self.log.info("secondaryPool::{}".format(instance.data["secondaryPool"])) + self.log.info( + "secondaryPool::{}".format(instance.data["secondaryPool"])) @classmethod def get_attribute_defs(cls): From 86c681df83e55127dd507a4f01964b0c1b7aa35c Mon Sep 17 00:00:00 2001 From: Jiri Sindelar Date: Tue, 18 Jul 2023 11:46:36 +0200 Subject: [PATCH 313/446] fix string conversion --- openpype/plugins/publish/extract_review_slate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 75c501a85c..7de3825108 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -442,11 +442,11 @@ class ExtractReviewSlate(publish.Extractor): input_width = width input_height = height - input_pixel_aspect = str(stream.get("sample_aspect_ratio")) + input_pixel_aspect = stream.get("sample_aspect_ratio") if input_pixel_aspect is not None: try: input_pixel_aspect = float( - eval(input_pixel_aspect.replace(':', '/'))) + eval(str(input_pixel_aspect).replace(':', '/'))) except Exception: self.log.debug( "__Converting pixel aspect to float failed: {}".format( From 61f7ac289318aeb28156d588830c3457287bee42 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 18 Jul 2023 11:16:25 +0100 Subject: [PATCH 314/446] Windows: Support long paths on zip updates. (#5265) * Extract long paths * Working version * Docs string and early bail out * Update igniter/bootstrap_repos.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update igniter/bootstrap_repos.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- igniter/bootstrap_repos.py | 42 +++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 4cf00375bf..408764e1a8 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -35,6 +35,29 @@ LOG_WARNING = 1 LOG_ERROR = 3 +def sanitize_long_path(path): + """Sanitize long paths (260 characters) when on Windows. + + Long paths are not capatible with ZipFile or reading a file, so we can + shorten the path to use. + + Args: + path (str): path to either directory or file. + + Returns: + str: sanitized path + """ + if platform.system().lower() != "windows": + return path + path = os.path.abspath(path) + + if path.startswith("\\\\"): + path = "\\\\?\\UNC\\" + path[2:] + else: + path = "\\\\?\\" + path + return path + + def sha256sum(filename): """Calculate sha256 for content of the file. @@ -54,6 +77,13 @@ def sha256sum(filename): return h.hexdigest() +class ZipFileLongPaths(ZipFile): + def _extract_member(self, member, targetpath, pwd): + return ZipFile._extract_member( + self, member, sanitize_long_path(targetpath), pwd + ) + + class OpenPypeVersion(semver.VersionInfo): """Class for storing information about OpenPype version. @@ -780,7 +810,7 @@ class BootstrapRepos: def _create_openpype_zip(self, zip_path: Path, openpype_path: Path) -> None: """Pack repositories and OpenPype into zip. - We are using :mod:`zipfile` instead :meth:`shutil.make_archive` + We are using :mod:`ZipFile` instead :meth:`shutil.make_archive` because we need to decide what file and directories to include in zip and what not. They are determined by :attr:`zip_filter` on file level and :attr:`openpype_filter` on top level directory in OpenPype @@ -834,7 +864,7 @@ class BootstrapRepos: checksums.append( ( - sha256sum(file.as_posix()), + sha256sum(sanitize_long_path(file.as_posix())), file.resolve().relative_to(openpype_root) ) ) @@ -958,7 +988,9 @@ class BootstrapRepos: if platform.system().lower() == "windows": file_name = file_name.replace("/", "\\") try: - current = sha256sum((path / file_name).as_posix()) + current = sha256sum( + sanitize_long_path((path / file_name).as_posix()) + ) except FileNotFoundError: return False, f"Missing file [ {file_name} ]" @@ -1270,7 +1302,7 @@ class BootstrapRepos: # extract zip there self._print("Extracting zip to destination ...") - with ZipFile(version.path, "r") as zip_ref: + with ZipFileLongPaths(version.path, "r") as zip_ref: zip_ref.extractall(destination) self._print(f"Installed as {version.path.stem}") @@ -1386,7 +1418,7 @@ class BootstrapRepos: # extract zip there self._print("extracting zip to destination ...") - with ZipFile(openpype_version.path, "r") as zip_ref: + with ZipFileLongPaths(openpype_version.path, "r") as zip_ref: self._progress_callback(75) zip_ref.extractall(destination) self._progress_callback(100) From 961bfe6c2effe21a6d388f64ffa4054f3d3901e7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Jul 2023 13:23:00 +0200 Subject: [PATCH 315/446] Remove forgotten dev logging (#5315) --- openpype/modules/deadline/plugins/publish/collect_pools.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index 706374d972..a25b149f11 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -47,10 +47,6 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, if instance.data["secondaryPool"] == "-": instance.data["secondaryPool"] = None - self.log.info("prima::{}".format(instance.data["primaryPool"])) - self.log.info( - "secondaryPool::{}".format(instance.data["secondaryPool"])) - @classmethod def get_attribute_defs(cls): # TODO: Preferably this would be an enum for the user From e917d2b91fa4b45612cca33ccc34a0d1bb237839 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov <11698866+movalex@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:00:36 +0300 Subject: [PATCH 316/446] Qt UI: Multiselection combobox PySide6 compatibility (#5314) * convert state to value for pyside compatibility use ItemIsUserTristate for keyboard event * use whole field length to select item * process keyboard tristate correctly * get initial check state data as value * try get state value for backwards compatibility * formatting * revert MouseButtonRelease event checks * added new utils constant for tristate constant * fixed both multiselection comboboxes * fixed sorting of projects in project manager * forgotten conversion of enum to int --------- Co-authored-by: Jakub Trllo --- .../project_manager/project_manager/model.py | 7 +++++ .../multiselection_combobox.py | 26 ++++++++++------ .../settings/multiselection_combobox.py | 31 ++++++++++++------- openpype/tools/utils/constants.py | 6 ++++ 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 29a26f700f..f6c98d6f6c 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -84,6 +84,13 @@ class ProjectProxyFilter(QtCore.QSortFilterProxyModel): super(ProjectProxyFilter, self).__init__(*args, **kwargs) self._filter_default = False + def lessThan(self, left, right): + if left.data(PROJECT_NAME_ROLE) is None: + return True + if right.data(PROJECT_NAME_ROLE) is None: + return False + return super(ProjectProxyFilter, self).lessThan(left, right) + def set_filter_default(self, enabled=True): """Set if filtering of default item is enabled.""" if enabled == self._filter_default: diff --git a/openpype/tools/project_manager/project_manager/multiselection_combobox.py b/openpype/tools/project_manager/project_manager/multiselection_combobox.py index 4b5d468982..4100ada221 100644 --- a/openpype/tools/project_manager/project_manager/multiselection_combobox.py +++ b/openpype/tools/project_manager/project_manager/multiselection_combobox.py @@ -1,6 +1,14 @@ from qtpy import QtCore, QtWidgets -from openpype.tools.utils.lib import checkstate_int_to_enum +from openpype.tools.utils.lib import ( + checkstate_int_to_enum, + checkstate_enum_to_int, +) +from openpype.tools.utils.constants import ( + CHECKED_INT, + UNCHECKED_INT, + ITEM_IS_USER_TRISTATE, +) class ComboItemDelegate(QtWidgets.QStyledItemDelegate): @@ -107,9 +115,9 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): return if state == QtCore.Qt.Unchecked: - new_state = QtCore.Qt.Checked + new_state = CHECKED_INT else: - new_state = QtCore.Qt.Unchecked + new_state = UNCHECKED_INT elif event.type() == QtCore.QEvent.KeyPress: # TODO: handle QtCore.Qt.Key_Enter, Key_Return? @@ -117,15 +125,15 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): # toggle the current items check state if ( index_flags & QtCore.Qt.ItemIsUserCheckable - and index_flags & QtCore.Qt.ItemIsTristate + and index_flags & ITEM_IS_USER_TRISTATE ): - new_state = QtCore.Qt.CheckState((int(state) + 1) % 3) + new_state = (checkstate_enum_to_int(state) + 1) % 3 elif index_flags & QtCore.Qt.ItemIsUserCheckable: if state != QtCore.Qt.Checked: - new_state = QtCore.Qt.Checked + new_state = CHECKED_INT else: - new_state = QtCore.Qt.Unchecked + new_state = UNCHECKED_INT if new_state is not None: model.setData(current_index, new_state, QtCore.Qt.CheckStateRole) @@ -180,9 +188,9 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): for idx in range(self.count()): value = self.itemData(idx, role=QtCore.Qt.UserRole) if value in values: - check_state = QtCore.Qt.Checked + check_state = CHECKED_INT else: - check_state = QtCore.Qt.Unchecked + check_state = UNCHECKED_INT self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole) def value(self): diff --git a/openpype/tools/settings/settings/multiselection_combobox.py b/openpype/tools/settings/settings/multiselection_combobox.py index 896be3c06c..d64fc83745 100644 --- a/openpype/tools/settings/settings/multiselection_combobox.py +++ b/openpype/tools/settings/settings/multiselection_combobox.py @@ -1,5 +1,13 @@ from qtpy import QtCore, QtGui, QtWidgets -from openpype.tools.utils.lib import checkstate_int_to_enum +from openpype.tools.utils.lib import ( + checkstate_int_to_enum, + checkstate_enum_to_int, +) +from openpype.tools.utils.constants import ( + CHECKED_INT, + UNCHECKED_INT, + ITEM_IS_USER_TRISTATE, +) class ComboItemDelegate(QtWidgets.QStyledItemDelegate): @@ -30,7 +38,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): QtCore.Qt.Key_PageDown, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_Home, - QtCore.Qt.Key_End + QtCore.Qt.Key_End, } top_bottom_padding = 2 @@ -127,25 +135,25 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): return if state == QtCore.Qt.Unchecked: - new_state = QtCore.Qt.Checked + new_state = CHECKED_INT else: - new_state = QtCore.Qt.Unchecked + new_state = UNCHECKED_INT elif event.type() == QtCore.QEvent.KeyPress: # TODO: handle QtCore.Qt.Key_Enter, Key_Return? if event.key() == QtCore.Qt.Key_Space: - # toggle the current items check state if ( index_flags & QtCore.Qt.ItemIsUserCheckable - and index_flags & QtCore.Qt.ItemIsTristate + and index_flags & ITEM_IS_USER_TRISTATE ): - new_state = QtCore.Qt.CheckState((int(state) + 1) % 3) + new_state = (checkstate_enum_to_int(state) + 1) % 3 elif index_flags & QtCore.Qt.ItemIsUserCheckable: + # toggle the current items check state if state != QtCore.Qt.Checked: - new_state = QtCore.Qt.Checked + new_state = CHECKED_INT else: - new_state = QtCore.Qt.Unchecked + new_state = UNCHECKED_INT if new_state is not None: model.setData(current_index, new_state, QtCore.Qt.CheckStateRole) @@ -249,7 +257,6 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): QtWidgets.QStyle.SC_ComboBoxArrow ) total_width = option.rect.width() - btn_rect.width() - font_metricts = self.fontMetrics() line = 0 self.lines = {line: []} @@ -305,9 +312,9 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): for idx in range(self.count()): value = self.itemData(idx, role=QtCore.Qt.UserRole) if value in values: - check_state = QtCore.Qt.Checked + check_state = CHECKED_INT else: - check_state = QtCore.Qt.Unchecked + check_state = UNCHECKED_INT self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole) self.update_size_hint() diff --git a/openpype/tools/utils/constants.py b/openpype/tools/utils/constants.py index 99f2602ee3..77324762b3 100644 --- a/openpype/tools/utils/constants.py +++ b/openpype/tools/utils/constants.py @@ -5,6 +5,12 @@ UNCHECKED_INT = getattr(QtCore.Qt.Unchecked, "value", 0) PARTIALLY_CHECKED_INT = getattr(QtCore.Qt.PartiallyChecked, "value", 1) CHECKED_INT = getattr(QtCore.Qt.Checked, "value", 2) +# Checkbox state +try: + ITEM_IS_USER_TRISTATE = QtCore.Qt.ItemIsUserTristate +except AttributeError: + ITEM_IS_USER_TRISTATE = QtCore.Qt.ItemIsTristate + DEFAULT_PROJECT_LABEL = "< Default >" PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 101 PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 102 From f2e4607434d283f3e4769495b369839813df28b4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Jul 2023 16:03:55 +0200 Subject: [PATCH 317/446] OP-4845 - add bundle name as a job env var Ayon must have AYON_BUNDLE_NAME to get proper env variables and addon used. --- .../deadline/abstract_submit_deadline.py | 102 ++---------------- 1 file changed, 11 insertions(+), 91 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 85b537360c..9fcff111e6 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -22,6 +22,9 @@ from openpype.pipeline.publish import ( KnownPublishError, OpenPypePyblishPluginMixin ) +from openpype.pipeline.publish.lib import ( + replace_with_published_scene_path +) JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) @@ -396,12 +399,12 @@ class DeadlineJobInfo(object): def add_render_job_env_var(self): """Check if in OP or AYON mode and use appropriate env var.""" - render_job = ( - "AYON_RENDER_JOB" if os.environ.get("USE_AYON_SERVER") == '1' - else "OPENPYPE_RENDER_JOB") - - self.EnvironmentKeyValue[render_job] = "1" - + if os.environ.get("USE_AYON_SERVER") == '1': + self.EnvironmentKeyValue["AYON_RENDER_JOB"] = "1" + self.EnvironmentKeyValue["AYON_BUNDLE_NAME"] = ( + os.environ["AYON_BUNDLE_NAME"]) + else: + self.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" @six.add_metaclass(AbstractMetaInstancePlugin) @@ -534,72 +537,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, published. """ - instance = self._instance - workfile_instance = self._get_workfile_instance(instance.context) - if workfile_instance is None: - return - - # determine published path from Anatomy. - template_data = workfile_instance.data.get("anatomyData") - rep = workfile_instance.data["representations"][0] - template_data["representation"] = rep.get("name") - template_data["ext"] = rep.get("ext") - template_data["comment"] = None - - anatomy = instance.context.data['anatomy'] - template_obj = anatomy.templates_obj["publish"]["path"] - template_filled = template_obj.format_strict(template_data) - file_path = os.path.normpath(template_filled) - - self.log.info("Using published scene for render {}".format(file_path)) - - if not os.path.exists(file_path): - self.log.error("published scene does not exist!") - raise - - if not replace_in_path: - return file_path - - # now we need to switch scene in expected files - # because token will now point to published - # scene file and that might differ from current one - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(file_path) - orig_scene = _clean_name(instance.context.data["currentFile"]) - expected_files = instance.data.get("expectedFiles") - - if isinstance(expected_files[0], dict): - # we have aovs and we need to iterate over them - new_exp = {} - for aov, files in expected_files[0].items(): - replaced_files = [] - for f in files: - replaced_files.append( - str(f).replace(orig_scene, new_scene) - ) - new_exp[aov] = replaced_files - # [] might be too much here, TODO - instance.data["expectedFiles"] = [new_exp] - else: - new_exp = [] - for f in expected_files: - new_exp.append( - str(f).replace(orig_scene, new_scene) - ) - instance.data["expectedFiles"] = new_exp - - metadata_folder = instance.data.get("publishRenderMetadataFolder") - if metadata_folder: - metadata_folder = metadata_folder.replace(orig_scene, - new_scene) - instance.data["publishRenderMetadataFolder"] = metadata_folder - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) - - return file_path + return replace_with_published_scene_path( + self._instance, replace_in_path=replace_in_path) def assemble_payload( self, job_info=None, plugin_info=None, aux_files=None): @@ -660,22 +599,3 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, self._instance.data["deadlineSubmissionJob"] = result return result["_id"] - - @staticmethod - def _get_workfile_instance(context): - """Find workfile instance in context""" - for instance in context: - - is_workfile = ( - "workfile" in instance.data.get("families", []) or - instance.data["family"] == "workfile" - ) - if not is_workfile: - continue - - # test if there is instance of workfile waiting - # to be published. - assert instance.data.get("publish", True) is True, ( - "Workfile (scene) must be published along") - - return instance From a612956dd1e1d21dbfedc279ac80ce9dbe7c5b31 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 18 Jul 2023 15:35:01 +0100 Subject: [PATCH 318/446] Update with compatible resolve version and latest docs --- website/docs/admin_hosts_resolve.md | 116 +++++++--------------------- 1 file changed, 27 insertions(+), 89 deletions(-) diff --git a/website/docs/admin_hosts_resolve.md b/website/docs/admin_hosts_resolve.md index 09e7df1d9f..8bb8440f78 100644 --- a/website/docs/admin_hosts_resolve.md +++ b/website/docs/admin_hosts_resolve.md @@ -4,100 +4,38 @@ title: DaVinci Resolve Setup sidebar_label: DaVinci Resolve --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; +:::warning +Only Resolve Studio is supported due to Python API limitation in Resolve (free). +::: ## Resolve requirements Due to the way resolve handles python and python scripts there are a few steps required steps needed to be done on any machine that will be using OpenPype with resolve. -### Installing Resolve's own python 3.6 interpreter. -Resolve uses a hardcoded method to look for the python executable path. All of tho following paths are defined automatically by Python msi installer. We are using Python 3.6.2. +## Basic setup - +- Supported version is up to v18 +- Install Python 3.6.2 (latest tested v17) or up to 3.9.13 (latest tested on v18) +- pip install PySide2: + - Python 3.9.*: open terminal and go to python.exe directory, then `python -m pip install PySide2` +- pip install OpenTimelineIO: + - Python 3.9.*: open terminal and go to python.exe directory, then `python -m pip install OpenTimelineIO` + - Python 3.6: open terminal and go to python.exe directory, then `python -m pip install git+https://github.com/PixarAnimationStudios/OpenTimelineIO.git@5aa24fbe89d615448876948fe4b4900455c9a3e8` and move built files from `./Lib/site-packages/opentimelineio/cxx-libs/bin and lib` to `./Lib/site-packages/opentimelineio/`. I was building it on Win10 machine with Visual Studio Community 2019 and + ![image](https://user-images.githubusercontent.com/40640033/102792588-ffcb1c80-43a8-11eb-9c6b-bf2114ed578e.png) with installed CMake in PATH. +- make sure Resolve Fusion (Fusion Tab/menu/Fusion/Fusion Settings) is set to Python 3.6 + ![image](https://user-images.githubusercontent.com/40640033/102631545-280b0f00-414e-11eb-89fc-98ac268d209d.png) +- Open OpenPype **Tray/Admin/Studio settings** > `applications/resolve/environment` and add Python3 path to `RESOLVE_PYTHON3_HOME` platform related. - +## Editorial setup -`%LOCALAPPDATA%\Programs\Python\Python36` +This is how it looks on my testing project timeline +![image](https://user-images.githubusercontent.com/40640033/102637638-96ec6600-4156-11eb-9656-6e8e3ce4baf8.png) +Notice I had renamed tracks to `main` (holding metadata markers) and `review` used for generating review data with ffmpeg confersion to jpg sequence. - - - -`/opt/Python/3.6/bin` - - - - -`~/Library/Python/3.6/bin` - - - - - -### Installing PySide2 into python 3.6 for correct gui work - -OpenPype is using its own window widget inside Resolve, for that reason PySide2 has to be installed into the python 3.6 (as explained above). - - - - - -paste to any terminal of your choice - -```bash -%LOCALAPPDATA%\Programs\Python\Python36\python.exe -m pip install PySide2 -``` - - - - -paste to any terminal of your choice - -```bash -/opt/Python/3.6/bin/python -m pip install PySide2 -``` - - - - -paste to any terminal of your choice - -```bash -~/Library/Python/3.6/bin/python -m pip install PySide2 -``` - - - - -
- -### Set Resolve's Fusion settings for Python 3.6 interpereter - -
- - -As it is shown in below picture you have to go to Fusion Tab and then in Fusion menu find Fusion Settings. Go to Fusion/Script and find Default Python Version and switch to Python 3.6 - -
- -
- -![Create menu](assets/resolve_fusion_tab.png) -![Create menu](assets/resolve_fusion_menu.png) -![Create menu](assets/resolve_fusion_script_settings.png) - -
-
\ No newline at end of file +1. you need to start OpenPype menu from Resolve/EditTab/Menu/Workspace/Scripts/Comp/**__OpenPype_Menu__** +2. then select any clips in `main` track and change their color to `Chocolate` +3. in OpenPype Menu select `Create` +4. in Creator select `Create Publishable Clip [New]` (temporary name) +5. set `Rename clips` to True, Master Track to `main` and Use review track to `review` as in picture + ![image](https://user-images.githubusercontent.com/40640033/102643773-0d419600-4160-11eb-919e-9c2be0aecab8.png) +6. after you hit `ok` all clips are colored to `ping` and marked with openpype metadata tag +7. git `Publish` on openpype menu and see that all had been collected correctly. That is the last step for now as rest is Work in progress. Next steps will follow. From 04a1de4ade928e378fc4812f1a63435ee3ee026b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 18 Jul 2023 18:04:53 +0200 Subject: [PATCH 319/446] :recycle: handle openssl 1.1.1 for centos 7 docker build --- Dockerfile.centos7 | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index ce1a624a4f..9217140f20 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -32,12 +32,16 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n wget \ gcc \ zlib-devel \ + pcre-devel \ + perl-core \ bzip2 \ bzip2-devel \ readline-devel \ sqlite sqlite-devel \ openssl-devel \ openssl-libs \ + openssl11-devel \ + openssl11-libs \ tk-devel libffi-devel \ patchelf \ automake \ @@ -71,7 +75,12 @@ RUN echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/.bashrc \ && echo 'eval "$(pyenv init -)"' >> $HOME/.bashrc \ && echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/.bashrc \ && echo 'eval "$(pyenv init --path)"' >> $HOME/.bashrc -RUN source $HOME/.bashrc && pyenv install ${OPENPYPE_PYTHON_VERSION} +RUN source $HOME/.bashrc \ + && export CPPFLAGS="-I/usr/include/openssl11" \ + && export LDFLAGS="-L/usr/lib64/openssl11 -lssl -lcrypto" \ + && export PATH=/usr/local/openssl/bin:$PATH \ + && export LD_LIBRARY_PATH=/usr/local/openssl/lib:$LD_LIBRARY_PATH \ + && pyenv install ${OPENPYPE_PYTHON_VERSION} COPY . /opt/openpype/ RUN rm -rf /openpype/.poetry || echo "No Poetry installed yet." @@ -93,12 +102,13 @@ RUN source $HOME/.bashrc \ RUN source $HOME/.bashrc \ && ./tools/fetch_thirdparty_libs.sh +RUN echo 'export PYTHONPATH="/opt/openpype/vendor/python:$PYTHONPATH"'>> $HOME/.bashrc RUN source $HOME/.bashrc \ && bash ./tools/build.sh RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.9/lib \ - && cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.9/lib \ - && cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.9/lib \ + && cp /usr/lib64/openssl11/libssl* ./build/exe.linux-x86_64-3.9/lib \ + && cp /usr/lib64/openssl11/libcrypto* ./build/exe.linux-x86_64-3.9/lib \ && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.9/lib \ && cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.9/vendor/python/PySide2/Qt/lib From 7c02c3b9d604acaeb9e05b96df1b991bddaeeb15 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Jul 2023 18:14:05 +0200 Subject: [PATCH 320/446] OP-4845 - add fields for server url and api key --- .../repository/custom/plugins/Ayon/Ayon.param | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.param b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.param index 81df2ecd95..8ba044ff81 100644 --- a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.param +++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.param @@ -15,3 +15,21 @@ CategoryOrder=1 Index=0 Default= Description=The path to the Ayon executable. Enter alternative paths on separate lines. + +[AyonServerUrl] +Type=string +Label=Ayon Server Url +Category=Ayon Credentials +CategoryOrder=2 +Index=0 +Default= +Description=Url to Ayon server + +[AyonApiKey] +Type=password +Label=Ayon API key +Category=Ayon Credentials +CategoryOrder=2 +Index=0 +Default= +Description=API key for service account on Ayon Server From 2bc019f6c0c60735840a88a1ef60e8e8697cebb5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Jul 2023 18:14:41 +0200 Subject: [PATCH 321/446] OP-4845 - try to push OPENPYPE_MONGO to extractenvironment process --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index d69aa12b5a..ed06b2b16b 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -355,6 +355,12 @@ def inject_openpype_environment(deadlinePlugin): " AVALON_TASK, AVALON_APP_NAME" )) + openpype_mongo = job.GetJobEnvironmentKeyValue("OPENPYPE_MONGO") + if openpype_mongo: + # inject env var for OP extractenvironments + deadlinePlugin.SetProcessEnvironmentVariable("OPENPYPE_MONGO", + openpype_mongo) + if not os.environ.get("OPENPYPE_MONGO"): print(">>> Missing OPENPYPE_MONGO env var, process won't work") From 098e58ba7b1d818b1baca9244ce6fc096434be21 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Jul 2023 18:15:50 +0200 Subject: [PATCH 322/446] OP-4845 - added Ayon logic For now both OP and Ayon will live together, later OP logic should be made obsolete. --- .../custom/plugins/GlobalJobPreLoad.py | 168 +++++++++++++++++- 1 file changed, 164 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index ed06b2b16b..4697cce38e 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -404,6 +404,153 @@ def inject_openpype_environment(deadlinePlugin): raise +def inject_ayon_environment(deadlinePlugin): + """ Pull env vars from Ayon and push them to rendering process. + + Used for correct paths, configuration from OpenPype etc. + """ + job = deadlinePlugin.GetJob() + + print(">>> Injecting Ayon environments ...") + try: + exe_list = get_ayon_executable() + exe = FileUtils.SearchFileList(exe_list) + + if not exe: + raise RuntimeError(( + "Ayon executable was not found in the semicolon " + "separated list \"{}\"." + "The path to the render executable can be configured" + " from the Plugin Configuration in the Deadline Monitor." + ).format(";".join(exe_list))) + + print("--- Ayon executable: {}".format(exe)) + + ayon_bundle_name = job.GetJobEnvironmentKeyValue("AYON_BUNDLE_NAME") + if not ayon_bundle_name: + raise RuntimeError("Missing env var in job properties " + "AYON_BUNDLE_NAME") + + config = RepositoryUtils.GetPluginConfig("Ayon") + ayon_server_url = ( + job.GetJobEnvironmentKeyValue("AYON_SERVER_URL") or + config.GetConfigEntryWithDefault("AyonServerUrl", "") + ) + ayon_api_key = ( + job.GetJobEnvironmentKeyValue("AYON_API_KEY") or + config.GetConfigEntryWithDefault("AyonApiKey", "") + ) + + if not all([ayon_server_url, ayon_api_key]): + raise RuntimeError(( + "Missing required values for server url and api key. " + "Please fill in Ayon Deadline plugin or provide by " + "AYON_SERVER_URL and AYON_API_KEY" + )) + + # tempfile.TemporaryFile cannot be used because of locking + temp_file_name = "{}_{}.json".format( + datetime.utcnow().strftime('%Y%m%d%H%M%S%f'), + str(uuid.uuid1()) + ) + export_url = os.path.join(tempfile.gettempdir(), temp_file_name) + print(">>> Temporary path: {}".format(export_url)) + + args = [ + "--headless", + "extractenvironments", + export_url + ] + + add_kwargs = { + "project": job.GetJobEnvironmentKeyValue("AVALON_PROJECT"), + "asset": job.GetJobEnvironmentKeyValue("AVALON_ASSET"), + "task": job.GetJobEnvironmentKeyValue("AVALON_TASK"), + "app": job.GetJobEnvironmentKeyValue("AVALON_APP_NAME"), + "envgroup": "farm", + } + + if job.GetJobEnvironmentKeyValue('IS_TEST'): + args.append("--automatic-tests") + + if all(add_kwargs.values()): + for key, value in add_kwargs.items(): + args.extend(["--{}".format(key), value]) + else: + raise RuntimeError(( + "Missing required env vars: AVALON_PROJECT, AVALON_ASSET," + " AVALON_TASK, AVALON_APP_NAME" + )) + + os.environ["AVALON_TIMEOUT"] = "5000" + + environment = { + "AYON_SERVER_URL": ayon_server_url, + "AYON_API_KEY": ayon_api_key, + "AYON_BUNDLE_NAME": ayon_bundle_name, + } + for env, val in environment.items(): + deadlinePlugin.SetEnvironmentVariable(env, val) + + args_str = subprocess.list2cmdline(args) + print(">>> Executing: {} {}".format(exe, args_str)) + process_exitcode = deadlinePlugin.RunProcess( + exe, args_str, os.path.dirname(exe), -1 + ) + + if process_exitcode != 0: + raise RuntimeError( + "Failed to run Ayon process to extract environments." + ) + + print(">>> Loading file ...") + with open(export_url) as fp: + contents = json.load(fp) + + for key, value in contents.items(): + deadlinePlugin.SetProcessEnvironmentVariable(key, value) + + script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") + if script_url: + script_url = script_url.format(**contents).replace("\\", "/") + print(">>> Setting script path {}".format(script_url)) + job.SetJobPluginInfoKeyValue("ScriptFilename", script_url) + + print(">>> Removing temporary file") + os.remove(export_url) + + print(">> Injection end.") + except Exception as e: + if hasattr(e, "output"): + print(">>> Exception {}".format(e.output)) + import traceback + print(traceback.format_exc()) + print("!!! Injection failed.") + RepositoryUtils.FailJob(job) + raise + + +def get_ayon_executable(): + """Return OpenPype Executable from Event Plug-in Settings + + Returns: + (list) of paths + Raises: + (RuntimeError) if no path configured at all + """ + config = RepositoryUtils.GetPluginConfig("Ayon") + exe_list = config.GetConfigEntryWithDefault("AyonExecutable", "") + + if not exe_list: + raise RuntimeError("Path to Ayon executable not configured." + "Please set it in Ayon Deadline Plugin.") + + # clean '\ ' for MacOS pasting + if platform.system().lower() == "darwin": + exe_list = exe_list.replace("\\ ", " ") + return exe_list + + def inject_render_job_id(deadlinePlugin): """Inject dependency ids to publish process as env var for validation.""" print(">>> Injecting render job id ...") @@ -430,14 +577,27 @@ def __main__(deadlinePlugin): openpype_remote_job = \ job.GetJobEnvironmentKeyValue('OPENPYPE_REMOTE_PUBLISH') or '0' - print("--- Job type - render {}".format(openpype_render_job)) - print("--- Job type - publish {}".format(openpype_publish_job)) - print("--- Job type - remote {}".format(openpype_remote_job)) if openpype_publish_job == '1' and openpype_render_job == '1': raise RuntimeError("Misconfiguration. Job couldn't be both " + "render and publish.") if openpype_publish_job == '1': inject_render_job_id(deadlinePlugin) - elif openpype_render_job == '1' or openpype_remote_job == '1': + if openpype_render_job == '1' or openpype_remote_job == '1': inject_openpype_environment(deadlinePlugin) + + ayon_render_job = \ + job.GetJobEnvironmentKeyValue('AYON_RENDER_JOB') or '0' + ayon_publish_job = \ + job.GetJobEnvironmentKeyValue('AYON_PUBLISH_JOB') or '0' + ayon_remote_job = \ + job.GetJobEnvironmentKeyValue('AYON_REMOTE_PUBLISH') or '0' + + if ayon_publish_job == '1' and ayon_render_job == '1': + raise RuntimeError("Misconfiguration. Job couldn't be both " + + "render and publish.") + + if ayon_publish_job == '1': + inject_render_job_id(deadlinePlugin) + if ayon_render_job == '1' or ayon_remote_job == '1': + inject_ayon_environment(deadlinePlugin) From 31f2ff680aeb5775034ddee2d274541a55be2605 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Jul 2023 19:37:03 +0200 Subject: [PATCH 323/446] OP-4845 - fix passing correct values to Ayon publish job --- .../deadline/plugins/publish/submit_publish_job.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 1b51c8efd1..54236d3cc2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -94,7 +94,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, label = "Submit image sequence jobs to Deadline or Muster" order = pyblish.api.IntegratorOrder + 0.2 icon = "tractor" - deadline_plugin = "OpenPype" + targets = ["local"] hosts = ["fusion", "max", "maya", "nuke", "houdini", @@ -126,10 +126,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "OPENPYPE_SG_USER" ] - # Add OpenPype version if we are running from build. - if is_running_from_build(): - environ_keys.append("OPENPYPE_VERSION") - # custom deadline attributes deadline_department = "" deadline_pool = "" @@ -211,10 +207,16 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, environment["AYON_PUBLISH_JOB"] = "1" environment["AYON_RENDER_JOB"] = "0" environment["AYON_REMOTE_PUBLISH"] = "0" + environment["AYON_BUNDLE_NAME"] = os.environ["AYON_BUNDLE_NAME"] + deadline_plugin = "Ayon" else: environment["OPENPYPE_PUBLISH_JOB"] = "1" environment["OPENPYPE_RENDER_JOB"] = "0" environment["OPENPYPE_REMOTE_PUBLISH"] = "0" + deadline_plugin = "Openpype" + # Add OpenPype version if we are running from build. + if is_running_from_build(): + self.environ_keys.append("OPENPYPE_VERSION") # add environments from self.environ_keys for env_key in self.environ_keys: @@ -258,7 +260,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, ) payload = { "JobInfo": { - "Plugin": self.deadline_plugin, + "Plugin": deadline_plugin, "BatchName": job["Props"]["Batch"], "Name": job_name, "UserName": job["Props"]["User"], From bf831778d8cb2aa1f634911199c06a7a5c360c38 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Jul 2023 19:38:08 +0200 Subject: [PATCH 324/446] OP-4845 - temporary fix for missing Ayon template Not sure if it was decided that Ayon won't have default 'render' template as OP does, but this should workaround it for testing. Needs to be fixed! --- .../deadline/plugins/publish/submit_publish_job.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 54236d3cc2..f912be1abe 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -575,7 +575,13 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, template_data["family"] = family template_data["version"] = version - render_templates = anatomy.templates_obj["render"] + # temporary fix, Ayon Settings don't have 'render' template, but they + # have "publish" TODO!!! + template_name = "render" + if os.environ.get("USE_AYON_SERVER") == '1': + template_name = "publish" + + render_templates = anatomy.templates_obj[template_name] if "folder" in render_templates: publish_folder = render_templates["folder"].format_strict( template_data From cf3e1d9593002df9352c0fe5db9184b2a78769bc Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 18 Jul 2023 20:46:16 +0300 Subject: [PATCH 325/446] fix typo --- openpype/hosts/houdini/plugins/create/create_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py index 8b6a68437b..b814dd9d57 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py @@ -33,7 +33,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): instance_node = hou.node(instance.get("instance_node")) parms = { - "RS_archive_file": '$HIP/pyblish/`{}.$F4.rs'.format(subset_name), + "RS_archive_file": '$HIP/pyblish/{}.$F4.rs'.format(subset_name), } if self.selected_nodes: From 87700f72dccc836376b036b1dc4aef9191d6983a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Jul 2023 19:48:03 +0200 Subject: [PATCH 326/446] OP-4845 - injecting required env vars for ayon_console Renamed OP to Ayon --- .../repository/custom/plugins/Ayon/Ayon.py | 136 ++++-------------- 1 file changed, 26 insertions(+), 110 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py index c29f7ca4e2..ae7aa7df75 100644 --- a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py +++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py @@ -21,18 +21,18 @@ import platform # main DeadlinePlugin class. ###################################################################### def GetDeadlinePlugin(): - return OpenPypeDeadlinePlugin() + return AyonDeadlinePlugin() def CleanupDeadlinePlugin(deadlinePlugin): deadlinePlugin.Cleanup() -class OpenPypeDeadlinePlugin(DeadlinePlugin): +class AyonDeadlinePlugin(DeadlinePlugin): """ - Standalone plugin for publishing from OpenPype. + Standalone plugin for publishing from Ayon - Calls OpenPype executable 'openpype_console' from first correctly found + Calls Ayonexecutable 'ayon_console' from first correctly found file based on plugin configuration. Uses 'publish' command and passes path to metadata json file, which contains all needed information for publish process. @@ -61,124 +61,40 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): self.AddStdoutHandlerCallback( ".*Progress: (\d+)%.*").HandleCallback += self.HandleProgress - @staticmethod - def get_openpype_version_from_path(path, build=True): - """Get OpenPype version from provided path. - path (str): Path to scan. - build (bool, optional): Get only builds, not sources - - Returns: - str or None: version of OpenPype if found. - - """ - # fix path for application bundle on macos - if platform.system().lower() == "darwin": - path = os.path.join(path, "MacOS") - - version_file = os.path.join(path, "openpype", "version.py") - if not os.path.isfile(version_file): - return None - - # skip if the version is not build - exe = os.path.join(path, "openpype_console.exe") - if platform.system().lower() in ["linux", "darwin"]: - exe = os.path.join(path, "openpype_console") - - # if only builds are requested - if build and not os.path.isfile(exe): # noqa: E501 - print(f" ! path is not a build: {path}") - return None - - version = {} - with open(version_file, "r") as vf: - exec(vf.read(), version) - - version_match = re.search(r"(\d+\.\d+.\d+).*", version["__version__"]) - return version_match[1] - def RenderExecutable(self): job = self.GetJob() - openpype_versions = [] - # if the job requires specific OpenPype version, - # lets go over all available and find compatible build. - requested_version = job.GetJobEnvironmentKeyValue("OPENPYPE_VERSION") - if requested_version: - self.LogInfo(( - "Scanning for compatible requested " - f"version {requested_version}")) - dir_list = self.GetConfigEntry("OpenPypeInstallationDirs") - # clean '\ ' for MacOS pasting - if platform.system().lower() == "darwin": - dir_list = dir_list.replace("\\ ", " ") + # set required env vars for Ayon + # cannot be in InitializeProcess as it is too soon + config = RepositoryUtils.GetPluginConfig("Ayon") + ayon_server_url = ( + job.GetJobEnvironmentKeyValue("AYON_SERVER_URL") or + config.GetConfigEntryWithDefault("AyonServerUrl", "") + ) + ayon_api_key = ( + job.GetJobEnvironmentKeyValue("AYON_API_KEY") or + config.GetConfigEntryWithDefault("AyonApiKey", "") + ) + ayon_bundle_name = job.GetJobEnvironmentKeyValue("AYON_BUNDLE_NAME") - for dir_list in dir_list.split(","): - install_dir = DirectoryUtils.SearchDirectoryList(dir_list) - if install_dir: - sub_dirs = [ - f.path for f in os.scandir(install_dir) - if f.is_dir() - ] - for subdir in sub_dirs: - version = self.get_openpype_version_from_path(subdir) - if not version: - continue - openpype_versions.append((version, subdir)) + environment = { + "AYON_SERVER_URL": ayon_server_url, + "AYON_API_KEY": ayon_api_key, + "AYON_BUNDLE_NAME": ayon_bundle_name, + } - exe_list = self.GetConfigEntry("OpenPypeExecutable") + for env, val in environment.items(): + self.SetProcessEnvironmentVariable(env, val) + + exe_list = self.GetConfigEntry("AyonExecutable") # clean '\ ' for MacOS pasting if platform.system().lower() == "darwin": exe_list = exe_list.replace("\\ ", " ") exe = FileUtils.SearchFileList(exe_list) - if openpype_versions: - # if looking for requested compatible version, - # add the implicitly specified to the list too. - version = self.get_openpype_version_from_path( - os.path.dirname(exe)) - if version: - openpype_versions.append((version, os.path.dirname(exe))) - - if requested_version: - # sort detected versions - if openpype_versions: - openpype_versions.sort( - key=lambda ver: [ - int(t) if t.isdigit() else t.lower() - for t in re.split(r"(\d+)", ver[0]) - ]) - requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 - compatible_versions = [] - for version in openpype_versions: - v = version[0].split(".")[:3] - if v[0] == requested_major and v[1] == requested_minor: - compatible_versions.append(version) - if not compatible_versions: - self.FailRender(("Cannot find compatible version available " - "for version {} requested by the job. " - "Please add it through plugin configuration " - "in Deadline or install it to configured " - "directory.").format(requested_version)) - # sort compatible versions nad pick the last one - compatible_versions.sort( - key=lambda ver: [ - int(t) if t.isdigit() else t.lower() - for t in re.split(r"(\d+)", ver[0]) - ]) - # create list of executables for different platform and let - # Deadline decide. - exe_list = [ - os.path.join( - compatible_versions[-1][1], "openpype_console.exe"), - os.path.join( - compatible_versions[-1][1], "openpype_console"), - os.path.join( - compatible_versions[-1][1], "MacOS", "openpype_console") - ] - exe = FileUtils.SearchFileList(";".join(exe_list)) if exe == "": self.FailRender( - "OpenPype executable was not found " + + "Ayon executable was not found " + "in the semicolon separated list " + "\"" + ";".join(exe_list) + "\". " + "The path to the render executable can be configured " + From d1f6e664ab565598a8483fde544c2b20ad83ca9e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Jul 2023 19:51:46 +0200 Subject: [PATCH 327/446] OP-4845 - updated injection of ayon env var It seems that SetEnvironmentVariable is required instead of SetProcessEnvironmentVariable. (In Ayon Deadline plugin it is opposite..probably because of Deadline... --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 4697cce38e..f3e49efefd 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -358,8 +358,9 @@ def inject_openpype_environment(deadlinePlugin): openpype_mongo = job.GetJobEnvironmentKeyValue("OPENPYPE_MONGO") if openpype_mongo: # inject env var for OP extractenvironments - deadlinePlugin.SetProcessEnvironmentVariable("OPENPYPE_MONGO", - openpype_mongo) + # SetEnvironmentVariable is important, not SetProcessEnv... + deadlinePlugin.SetEnvironmentVariable("OPENPYPE_MONGO", + openpype_mongo) if not os.environ.get("OPENPYPE_MONGO"): print(">>> Missing OPENPYPE_MONGO env var, process won't work") From 71b273c280f3880e85e7cdf4efd531554f3fb772 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 19 Jul 2023 03:47:48 +0000 Subject: [PATCH 328/446] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index b2dfc857a3..40375bef43 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.1" +__version__ = "3.16.2-nightly.1" From 19473852786f2c522d900c20e4bc5b9bc527f9df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Jul 2023 03:48:31 +0000 Subject: [PATCH 329/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 31d1ec74f5..2d9915609a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.2-nightly.1 - 3.16.1 - 3.16.0 - 3.16.0-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.6-nightly.3 - 3.14.6-nightly.2 - 3.14.6-nightly.1 - - 3.14.5 validations: required: true - type: dropdown From f226b8748962442824d7170bac759636cd502a51 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 20 Jul 2023 11:09:33 +0200 Subject: [PATCH 330/446] :bug: fix wrong creator identifier --- .../hosts/houdini/plugins/publish/collect_pointcache_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py b/openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py index 6c527377e0..3323e97c20 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py @@ -17,5 +17,5 @@ class CollectPointcacheType(pyblish.api.InstancePlugin): def process(self, instance): if instance.data["creator_identifier"] == "io.openpype.creators.houdini.bgeo": # noqa: E501 instance.data["families"] += ["bgeo"] - elif instance.data["creator_identifier"] == "io.openpype.creators.houdini.alembic": # noqa: E501 + elif instance.data["creator_identifier"] == "io.openpype.creators.houdini.pointcache": # noqa: E501 instance.data["families"] += ["abc"] From 8c73d22d2736cfb460c52cc47c66ec9a5e1e4a81 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Jul 2023 15:51:12 +0200 Subject: [PATCH 331/446] extracted common logic to 'MayaCreatorBase' --- openpype/hosts/maya/api/plugin.py | 57 ++++++++++++++++++------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 2b5aee9700..11dad6d7c3 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -177,6 +177,36 @@ class MayaCreatorBase(object): return node_data + def _default_collect_instances(self): + self.cache_subsets(self.collection_shared_data) + cached_subsets = self.collection_shared_data["maya_cached_subsets"] + for node in cached_subsets.get(self.identifier, []): + node_data = self.read_instance_node(node) + + created_instance = CreatedInstance.from_existing(node_data, self) + self._add_instance_to_context(created_instance) + + def _default_update_instances(self, update_list): + for created_inst, _changes in update_list: + data = created_inst.data_to_store() + node = data.get("instance_node") + + self.imprint_instance_node(node, data) + + def _default_remove_instances(self, instances): + """Remove specified instance from the scene. + + This is only removing `id` parameter so instance is no longer + instance, because it might contain valuable data for artist. + + """ + for instance in instances: + node = instance.data.get("instance_node") + if node: + cmds.delete(node) + + self._remove_instance_from_context(instance) + @six.add_metaclass(ABCMeta) class MayaCreator(NewCreator, MayaCreatorBase): @@ -202,34 +232,13 @@ class MayaCreator(NewCreator, MayaCreatorBase): return instance def collect_instances(self): - self.cache_subsets(self.collection_shared_data) - cached_subsets = self.collection_shared_data["maya_cached_subsets"] - for node in cached_subsets.get(self.identifier, []): - node_data = self.read_instance_node(node) - - created_instance = CreatedInstance.from_existing(node_data, self) - self._add_instance_to_context(created_instance) + return self._default_collect_instances() def update_instances(self, update_list): - for created_inst, _changes in update_list: - data = created_inst.data_to_store() - node = data.get("instance_node") - - self.imprint_instance_node(node, data) + return self._default_update_instances(update_list) def remove_instances(self, instances): - """Remove specified instance from the scene. - - This is only removing `id` parameter so instance is no longer - instance, because it might contain valuable data for artist. - - """ - for instance in instances: - node = instance.data.get("instance_node") - if node: - cmds.delete(node) - - self._remove_instance_from_context(instance) + return self._default_remove_instances(instances) def get_pre_create_attr_defs(self): return [ From 464e62188074cda6a88aedde0a723c2acd85ebf2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Jul 2023 15:53:00 +0200 Subject: [PATCH 332/446] implemented base classes of auto creator and hidden creator for maya --- openpype/hosts/maya/api/plugin.py | 57 +++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 11dad6d7c3..0d2e4efdb1 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -8,13 +8,24 @@ from maya import cmds from maya.app.renderSetup.model import renderSetup from openpype.lib import BoolDef, Logger -from openpype.pipeline import AVALON_CONTAINER_ID, Anatomy, CreatedInstance -from openpype.pipeline import Creator as NewCreator -from openpype.pipeline import ( - CreatorError, LegacyCreator, LoaderPlugin, get_representation_path, - legacy_io) -from openpype.pipeline.load import LoadError from openpype.settings import get_project_settings +from openpype.pipeline import ( + AVALON_CONTAINER_ID, + Anatomy, + + CreatedInstance, + Creator as NewCreator, + AutoCreator, + HiddenCreator, + + CreatorError, + LegacyCreator, + LoaderPlugin, + get_representation_path, + + legacy_io, +) +from openpype.pipeline.load import LoadError from . import lib from .lib import imprint, read @@ -248,6 +259,40 @@ class MayaCreator(NewCreator, MayaCreatorBase): ] +class MayaAutoCreator(AutoCreator, MayaCreatorBase): + """Automatically triggered creator for Maya. + + The plugin is not visible in UI, and 'create' method does not expect + any arguments. + """ + + def collect_instances(self): + return self._default_collect_instances() + + def update_instances(self, update_list): + return self._default_update_instances(update_list) + + def remove_instances(self, instances): + return self._default_remove_instances(instances) + + +class MayaHiddenCreator(HiddenCreator, MayaCreatorBase): + """Hidden creator for Maya. + + The plugin is not visible in UI, and it does not have strictly defined + arguments for 'create' method. + """ + + def collect_instances(self): + return self._default_collect_instances() + + def update_instances(self, update_list): + return self._default_update_instances(update_list) + + def remove_instances(self, instances): + return self._default_remove_instances(instances) + + def ensure_namespace(namespace): """Make sure the namespace exists. From 64cc8ab97e0f22d6e8be56b4fe69e8b24ee6d530 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Jul 2023 15:53:15 +0200 Subject: [PATCH 333/446] added 'HiddenCreator' to pipeline public api --- openpype/pipeline/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 5c15a5fa82..59f1655f91 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -13,6 +13,7 @@ from .create import ( BaseCreator, Creator, AutoCreator, + HiddenCreator, CreatedInstance, CreatorError, @@ -114,6 +115,7 @@ __all__ = ( "BaseCreator", "Creator", "AutoCreator", + "HiddenCreator", "CreatedInstance", "CreatorError", From 5cb33b71bfc9fdd2433d30075fa89b1261db21a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Jul 2023 15:53:35 +0200 Subject: [PATCH 334/446] fix type hint in docstrings --- openpype/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 98fcee5fe5..faeb49584b 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1165,7 +1165,7 @@ class CreatedInstance: Args: instance_data (Dict[str, Any]): Data in a structure ready for 'CreatedInstance' object. - creator (Creator): Creator plugin which is creating the instance + creator (BaseCreator): Creator plugin which is creating the instance of for which the instance belong. """ From c385f748de97a12a24b494406603220d10f25095 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 20 Jul 2023 15:53:54 +0200 Subject: [PATCH 335/446] Set tool as active This makes the node-flow show the selected node + you'll see the nodes controls in the inspector --- openpype/hosts/fusion/api/action.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/api/action.py b/openpype/hosts/fusion/api/action.py index 347d552108..132c4af55d 100644 --- a/openpype/hosts/fusion/api/action.py +++ b/openpype/hosts/fusion/api/action.py @@ -51,6 +51,7 @@ class SelectInvalidAction(pyblish.api.Action): names = set() for tool in invalid: flow.Select(tool, True) + comp.SetActiveTool(tool) names.add(tool.Name) self.log.info( "Selecting invalid tools: %s" % ", ".join(sorted(names)) From db0e004b9ee298c337169ea8d707402d3558cb4f Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 20 Jul 2023 15:54:02 +0200 Subject: [PATCH 336/446] Black formatting --- openpype/hosts/fusion/api/action.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/action.py b/openpype/hosts/fusion/api/action.py index 132c4af55d..66b787c2f1 100644 --- a/openpype/hosts/fusion/api/action.py +++ b/openpype/hosts/fusion/api/action.py @@ -18,8 +18,10 @@ class SelectInvalidAction(pyblish.api.Action): icon = "search" # Icon from Awesome Icon def process(self, context, plugin): - errored_instances = get_errored_instances_from_context(context, - plugin=plugin) + errored_instances = get_errored_instances_from_context( + context, + plugin=plugin, + ) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") From 60eb35d0a60dee549cfe3be9bff081286e24d7cd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Jul 2023 16:00:54 +0200 Subject: [PATCH 337/446] fix formatting --- openpype/pipeline/create/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index faeb49584b..8d439defbe 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1165,8 +1165,8 @@ class CreatedInstance: Args: instance_data (Dict[str, Any]): Data in a structure ready for 'CreatedInstance' object. - creator (BaseCreator): Creator plugin which is creating the instance - of for which the instance belong. + creator (BaseCreator): Creator plugin which is creating the + instance of for which the instance belong. """ instance_data = copy.deepcopy(instance_data) From 7eea0cab973ca7b0920c6e9ab537f439205bd556 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 20 Jul 2023 18:23:43 +0100 Subject: [PATCH 338/446] Order instances for processing --- openpype/hosts/nuke/api/pipeline.py | 29 ++++++++++++++++--- .../plugins/create/create_write_prerender.py | 5 ++++ .../plugins/create/create_write_render.py | 5 ++++ openpype/pipeline/create/context.py | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index cdfc8aa512..fcc3becd2d 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -2,7 +2,7 @@ import nuke import os import importlib -from collections import OrderedDict +from collections import OrderedDict, defaultdict import pyblish.api @@ -537,7 +537,8 @@ def list_instances(creator_id=None): Returns: (list) of dictionaries matching instances format """ - listed_instances = [] + instances_by_order = defaultdict(list) + subset_instances = [] for node in nuke.allNodes(recurseGroups=True): if node.Class() in ["Viewer", "Dot"]: @@ -563,9 +564,29 @@ def list_instances(creator_id=None): if creator_id and instance_data["creator_identifier"] != creator_id: continue - listed_instances.append((node, instance_data)) + if "render_order" not in node.knobs(): + subset_instances.append((node, instance_data)) + continue - return listed_instances + order = int(node["render_order"].value()) + instances_by_order[order].append((node, instance_data)) + + # Sort instances based on order attribute or subset name. + ordered_instances = [] + for key in sorted(instances_by_order.keys()): + instances_by_subset = {} + for node, data in instances_by_order[key]: + instances_by_subset[data["subset"]] = (node, data) + for subkey in sorted(instances_by_subset.keys()): + ordered_instances.append(instances_by_subset[subkey]) + + instances_by_subset = {} + for node, data in subset_instances: + instances_by_subset[data["subset"]] = (node, data) + for key in sorted(instances_by_subset.keys()): + ordered_instances.append(instances_by_subset[key]) + + return ordered_instances def remove_instance(instance): diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index f46dd2d6d5..c3bba5f477 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -30,6 +30,9 @@ class CreateWritePrerender(napi.NukeWriteCreator): temp_rendering_path_template = ( "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}") + # Before write node render. + order = 90 + def get_pre_create_attr_defs(self): attr_defs = [ BoolDef( @@ -46,6 +49,8 @@ class CreateWritePrerender(napi.NukeWriteCreator): if "use_range_limit" in self.instance_attributes: linked_knobs_ = ["channels", "___", "first", "last", "use_limit"] + linked_knobs_.append("render_order") + # add fpath_template write_data = { "creator": self.__class__.__name__, diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index c24405873a..aef4b06a2c 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -39,6 +39,10 @@ class CreateWriteRender(napi.NukeWriteCreator): return attr_defs def create_instance_node(self, subset_name, instance_data): + linked_knobs_ = [ + "channels", "___", "first", "last", "use_limit", "render_order" + ] + # add fpath_template write_data = { "creator": self.__class__.__name__, @@ -61,6 +65,7 @@ class CreateWriteRender(napi.NukeWriteCreator): write_data, input=self.selected_node, prenodes=self.prenodes, + linked_knobs=linked_knobs_, **{ "width": width, "height": height diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 98fcee5fe5..6bdf7bb719 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -2121,7 +2121,7 @@ class CreateContext: def reset_instances(self): """Reload instances""" - self._instances_by_id = {} + self._instances_by_id = collections.OrderedDict() # Collect instances error_message = "Collection of instances for creator {} failed. {}" From a79eca9980446912823e595a9351892fcdbbc791 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Jul 2023 11:00:04 +0100 Subject: [PATCH 339/446] Add new publisher error raising --- .../maya/plugins/publish/validate_instance_in_context.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py index 41bb414829..b257add7e8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py @@ -3,7 +3,9 @@ from __future__ import absolute_import import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, PublishValidationError +) from maya import cmds @@ -108,4 +110,5 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin): asset = instance.data.get("asset") context_asset = instance.context.data["assetEntity"]["name"] msg = "{} has asset {}".format(instance.name, asset) - assert asset == context_asset, msg + if asset != context_asset: + raise PublishValidationError(msg) From 67cd2ff6506e2065c64c88f6848b84a465d9305b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Jul 2023 11:07:04 +0100 Subject: [PATCH 340/446] Fix fetching top level parents --- .../plugins/publish/validate_model_content.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_model_content.py b/openpype/hosts/maya/plugins/publish/validate_model_content.py index 9ba458a416..19373efad9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_content.py @@ -63,15 +63,10 @@ class ValidateModelContent(pyblish.api.InstancePlugin): return True # Top group - assemblies = cmds.ls(content_instance, assemblies=True, long=True) - if len(assemblies) != 1 and cls.validate_top_group: + top_parents = set([x.split("|")[1] for x in content_instance]) + if cls.validate_top_group and len(top_parents) != 1: cls.log.error("Must have exactly one top group") - return assemblies - if len(assemblies) == 0: - cls.log.warning("No top group found. " - "(Are there objects in the instance?" - " Or is it parented in another group?)") - return assemblies or True + return top_parents def _is_visible(node): """Return whether node is visible""" @@ -82,11 +77,11 @@ class ValidateModelContent(pyblish.api.InstancePlugin): visibility=True) # The roots must be visible (the assemblies) - for assembly in assemblies: - if not _is_visible(assembly): - cls.log.error("Invisible assembly (root node) is not " - "allowed: {0}".format(assembly)) - invalid.add(assembly) + for parent in top_parents: + if not _is_visible(parent): + cls.log.error("Invisible parent (root node) is not " + "allowed: {0}".format(parent)) + invalid.add(parent) # Ensure at least one shape is visible if not any(_is_visible(shape) for shape in shapes): From f91189fdf3fa5986f66e070920e02017c899a37e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Jul 2023 12:18:16 +0100 Subject: [PATCH 341/446] Use MayaHiddenCreator --- openpype/hosts/maya/plugins/create/create_animation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index 7424d1c590..214ac18aef 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -6,10 +6,9 @@ from openpype.lib import ( BoolDef, TextDef ) -from openpype.pipeline.create import HiddenCreator -class CreateAnimation(plugin.MayaCreator, HiddenCreator): +class CreateAnimation(plugin.MayaHiddenCreator): """Animation output for character rigs We hide the animation creator from the UI since the creation of it is From f5ccde3b9f9d71ea9211bd2ddbd4803cf927436b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Jul 2023 14:30:08 +0100 Subject: [PATCH 342/446] Remove enabled --- openpype/settings/defaults/project_settings/maya.json | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index fe369b534e..8e1022f877 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -555,7 +555,6 @@ "publish_mip_map": true }, "CreateAnimation": { - "enabled": true, "write_color_sets": false, "write_face_sets": false, "include_parent_hierarchy": false, From 484108cb8179f7a8872432fe98dd898908793527 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Jul 2023 14:59:56 +0100 Subject: [PATCH 343/446] Fix MayaHiddenCreator --- openpype/hosts/maya/api/plugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 0d2e4efdb1..2ad7cd842d 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -283,6 +283,9 @@ class MayaHiddenCreator(HiddenCreator, MayaCreatorBase): arguments for 'create' method. """ + def create(self, *args, **kwargs): + return MayaCreator.create(self, *args, **kwargs) + def collect_instances(self): return self._default_collect_instances() @@ -292,6 +295,9 @@ class MayaHiddenCreator(HiddenCreator, MayaCreatorBase): def remove_instances(self, instances): return self._default_remove_instances(instances) + def get_pre_create_attr_defs(self): + pass + def ensure_namespace(namespace): """Make sure the namespace exists. From 27d3edb89a20c805794773aa918fa1fc3caaab7c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 21 Jul 2023 15:51:57 +0100 Subject: [PATCH 344/446] Make configurable to set resolution and start/end frames at startup --- openpype/hosts/blender/api/pipeline.py | 64 +++++++++++++------ .../defaults/project_settings/blender.json | 2 + .../schema_project_blender.json | 10 +++ 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index eb696ec184..1c885724b5 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -113,22 +113,21 @@ def message_window(title, message): _process_app_events() -def set_start_end_frames(): +def _get_asset_data(): project_name = get_current_project_name() asset_name = get_current_asset_name() asset_doc = get_asset_by_name(project_name, asset_name) + return asset_doc.get("data") + + +def set_start_end_frames(data): scene = bpy.context.scene # Default scene settings frameStart = scene.frame_start frameEnd = scene.frame_end fps = scene.render.fps / scene.render.fps_base - resolution_x = scene.render.resolution_x - resolution_y = scene.render.resolution_y - - # Check if settings are set - data = asset_doc.get("data") if not data: return @@ -139,26 +138,47 @@ def set_start_end_frames(): frameEnd = data.get("frameEnd") if data.get("fps"): fps = data.get("fps") - if data.get("resolutionWidth"): - resolution_x = data.get("resolutionWidth") - if data.get("resolutionHeight"): - resolution_y = data.get("resolutionHeight") scene.frame_start = frameStart scene.frame_end = frameEnd scene.render.fps = round(fps) scene.render.fps_base = round(fps) / fps + + +def set_resolution(data): + scene = bpy.context.scene + + # Default scene settings + resolution_x = scene.render.resolution_x + resolution_y = scene.render.resolution_y + + if not data: + return + + if data.get("resolutionWidth"): + resolution_x = data.get("resolutionWidth") + if data.get("resolutionHeight"): + resolution_y = data.get("resolutionHeight") + scene.render.resolution_x = resolution_x scene.render.resolution_y = resolution_y def on_new(): - set_start_end_frames() - project = os.environ.get("AVALON_PROJECT") - settings = get_project_settings(project) + settings = get_project_settings(project).get("blender") - unit_scale_settings = settings.get("blender").get("unit_scale_settings") + set_resolution_startup = settings.get("set_resolution_startup") + set_frames_startup = settings.get("set_frames_startup") + + data = _get_asset_data() + + if set_resolution_startup: + set_resolution(data) + if set_frames_startup: + set_start_end_frames(data) + + unit_scale_settings = settings.get("unit_scale_settings") unit_scale_enabled = unit_scale_settings.get("enabled") if unit_scale_enabled: unit_scale = unit_scale_settings.get("base_file_unit_scale") @@ -166,12 +186,20 @@ def on_new(): def on_open(): - set_start_end_frames() - project = os.environ.get("AVALON_PROJECT") - settings = get_project_settings(project) + settings = get_project_settings(project).get("blender") - unit_scale_settings = settings.get("blender").get("unit_scale_settings") + set_resolution_startup = settings.get("set_resolution_startup") + set_frames_startup = settings.get("set_frames_startup") + + data = _get_asset_data() + + if set_resolution_startup: + set_resolution(data) + if set_frames_startup: + set_start_end_frames(data) + + unit_scale_settings = settings.get("unit_scale_settings") unit_scale_enabled = unit_scale_settings.get("enabled") apply_on_opening = unit_scale_settings.get("apply_on_opening") if unit_scale_enabled and apply_on_opening: diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 29e61fe233..fb11e727b3 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -4,6 +4,8 @@ "apply_on_opening": false, "base_file_unit_scale": 0.01 }, + "set_resolution_startup": true, + "set_frames_startup": true, "imageio": { "activate_host_color_management": true, "ocio_config": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index c549b577b2..aeb70dfd8c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -31,6 +31,16 @@ } ] }, + { + "key": "set_resolution_startup", + "type": "boolean", + "label": "Set Resolution on Startup" + }, + { + "key": "set_frames_startup", + "type": "boolean", + "label": "Set Start/End Frames and FPS on Startup" + }, { "key": "imageio", "type": "dict", From 66e28200639ffbe18d72aaf0689465e4f0822e1c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 21 Jul 2023 16:46:47 +0100 Subject: [PATCH 345/446] ExtractBurning for Blender reviews --- openpype/hosts/blender/plugins/publish/collect_review.py | 7 +++++++ openpype/plugins/publish/extract_burnin.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index 82b3ca11eb..1cb8dc8d8a 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -29,6 +29,8 @@ class CollectReview(pyblish.api.InstancePlugin): camera = cameras[0].name self.log.debug(f"camera: {camera}") + focal_length = cameras[0].data.lens + # get isolate objects list from meshes instance members . isolate_objects = [ obj @@ -40,6 +42,10 @@ class CollectReview(pyblish.api.InstancePlugin): task = instance.context.data["task"] + # Store focal length in `burninDataMembers` + burninDataMembers = instance.data.get("burninDataMembers", {}) + burninDataMembers["focalLength"] = focal_length + instance.data.update({ "subset": f"{task}Review", "review_camera": camera, @@ -47,6 +53,7 @@ class CollectReview(pyblish.api.InstancePlugin): "frameEnd": instance.context.data["frameEnd"], "fps": instance.context.data["fps"], "isolate": isolate_objects, + "burninDataMembers": burninDataMembers, }) self.log.debug(f"instance data: {instance.data}") diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index e67739e842..4a64711bfd 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -52,7 +52,8 @@ class ExtractBurnin(publish.Extractor): "photoshop", "flame", "houdini", - "max" + "max", + "blender" # "resolve" ] From 6a2d14d6c9de973645903a15931e2d71cb2a0f7e Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 22 Jul 2023 03:24:48 +0000 Subject: [PATCH 346/446] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 40375bef43..e46b97c063 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.2-nightly.1" +__version__ = "3.16.2-nightly.2" From 9f5cc1ac6a9e866c93ab949ce4f07251dba8ed38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 22 Jul 2023 03:25:34 +0000 Subject: [PATCH 347/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2d9915609a..66db18026d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.2-nightly.2 - 3.16.2-nightly.1 - 3.16.1 - 3.16.0 @@ -134,7 +135,6 @@ body: - 3.14.6 - 3.14.6-nightly.3 - 3.14.6-nightly.2 - - 3.14.6-nightly.1 validations: required: true - type: dropdown From 70f97b273804873acd5dadd68c60a6cf75817a7e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 22 Jul 2023 09:24:21 +0100 Subject: [PATCH 348/446] Fix get_pre_create_attr_defs query --- openpype/hosts/maya/api/plugin.py | 3 --- openpype/pipeline/create/context.py | 6 +++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 2ad7cd842d..c3b0d43eef 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -295,9 +295,6 @@ class MayaHiddenCreator(HiddenCreator, MayaCreatorBase): def remove_instances(self, instances): return self._default_remove_instances(instances) - def get_pre_create_attr_defs(self): - pass - def ensure_namespace(namespace): """Make sure the namespace exists. diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 8d439defbe..a8d2947ed6 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1979,7 +1979,11 @@ class CreateContext: if pre_create_data is None: pre_create_data = {} - precreate_attr_defs = creator.get_pre_create_attr_defs() or [] + precreate_attr_defs = [] + # Hidden creators do not have or need the pre-create attributes. + if hasattr(creator, "get_pre_create_attr_defs"): + precreate_attr_defs = creator.get_pre_create_attr_defs() + # Create default values of precreate data _pre_create_data = get_default_values(precreate_attr_defs) # Update passed precreate data to default values From 441618974e49a8ef6774836dbfd5b84584d29b56 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 22 Jul 2023 10:19:47 +0100 Subject: [PATCH 349/446] All review publish attributes should be copied to model instance --- openpype/hosts/maya/plugins/publish/collect_review.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index fa00fc661e..586939a3b8 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -107,10 +107,8 @@ class CollectReview(pyblish.api.InstancePlugin): data["displayLights"] = display_lights data["burninDataMembers"] = burninDataMembers - publish_attributes = data.setdefault("publish_attributes", {}) for key, value in instance.data["publish_attributes"].items(): - if key not in publish_attributes: - publish_attributes[key] = value + data["publish_attributes"][key] = value # The review instance must be active cmds.setAttr(str(instance) + '.active', 1) From a3467b25b66a846acd29b49da7e76a94fbd355cc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 22 Jul 2023 10:20:12 +0100 Subject: [PATCH 350/446] Better labelling for ValidateFrameRange setting. --- .../schemas/projects_schema/schemas/schema_maya_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 07c8d8715b..b115ee3faa 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -103,7 +103,7 @@ }, { "key": "exclude_families", - "label": "Families", + "label": "Exclude Families", "type": "list", "object_type": "text" } From ccd56c4bb5ccc78927fc2f5bc9757179212ff4a3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Jul 2023 09:42:50 +0100 Subject: [PATCH 351/446] Improved code based on suggestions --- openpype/hosts/blender/plugins/publish/collect_review.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index 1cb8dc8d8a..6459927015 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -43,8 +43,8 @@ class CollectReview(pyblish.api.InstancePlugin): task = instance.context.data["task"] # Store focal length in `burninDataMembers` - burninDataMembers = instance.data.get("burninDataMembers", {}) - burninDataMembers["focalLength"] = focal_length + burninData = instance.data.setdefault("burninDataMembers", {}) + burninData["focalLength"] = focal_length instance.data.update({ "subset": f"{task}Review", @@ -53,7 +53,6 @@ class CollectReview(pyblish.api.InstancePlugin): "frameEnd": instance.context.data["frameEnd"], "fps": instance.context.data["fps"], "isolate": isolate_objects, - "burninDataMembers": burninDataMembers, }) self.log.debug(f"instance data: {instance.data}") From 7fdfc78c2c3635aa720ede222f16cdb568dae0ce Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 24 Jul 2023 10:12:51 +0100 Subject: [PATCH 352/446] Update openpype/pipeline/create/context.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index a8d2947ed6..a794b10c35 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1981,7 +1981,7 @@ class CreateContext: precreate_attr_defs = [] # Hidden creators do not have or need the pre-create attributes. - if hasattr(creator, "get_pre_create_attr_defs"): + if isinstance(creator, Creator): precreate_attr_defs = creator.get_pre_create_attr_defs() # Create default values of precreate data From 99099b81e09ce74efaaf4d71eaa13f695b384d71 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Jul 2023 10:47:03 +0100 Subject: [PATCH 353/446] Added menu entries to set resolution and frame range --- openpype/hosts/blender/api/ops.py | 29 ++++++++++++++++++++++++-- openpype/hosts/blender/api/pipeline.py | 12 +++++------ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 2c1b7245cd..62d7987b47 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -20,6 +20,7 @@ from openpype.pipeline import get_current_asset_name, get_current_task_name from openpype.tools.utils import host_tools from .workio import OpenFileCacher +from . import pipeline PREVIEW_COLLECTIONS: Dict = dict() @@ -344,6 +345,26 @@ class LaunchWorkFiles(LaunchQtApp): self._window.refresh() +class SetFrameRange(bpy.types.Operator): + bl_idname = "wm.ayon_set_frame_range" + bl_label = "Set Frame Range" + + def execute(self, context): + data = pipeline.get_asset_data() + pipeline.set_frame_range(data) + return {"FINISHED"} + + +class SetResolution(bpy.types.Operator): + bl_idname = "wm.ayon_set_resolution" + bl_label = "Set Resolution" + + def execute(self, context): + data = pipeline.get_asset_data() + pipeline.set_resolution(data) + return {"FINISHED"} + + class TOPBAR_MT_avalon(bpy.types.Menu): """Avalon menu.""" @@ -381,9 +402,11 @@ class TOPBAR_MT_avalon(bpy.types.Menu): layout.operator(LaunchManager.bl_idname, text="Manage...") layout.operator(LaunchLibrary.bl_idname, text="Library...") layout.separator() + layout.operator(SetFrameRange.bl_idname, text="Set Frame Range") + layout.operator(SetResolution.bl_idname, text="Set Resolution") + layout.separator() layout.operator(LaunchWorkFiles.bl_idname, text="Work Files...") - # TODO (jasper): maybe add 'Reload Pipeline', 'Set Frame Range' and - # 'Set Resolution'? + # TODO (jasper): maybe add 'Reload Pipeline' def draw_avalon_menu(self, context): @@ -399,6 +422,8 @@ classes = [ LaunchManager, LaunchLibrary, LaunchWorkFiles, + SetFrameRange, + SetResolution, TOPBAR_MT_avalon, ] diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 1c885724b5..29339a512c 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -113,7 +113,7 @@ def message_window(title, message): _process_app_events() -def _get_asset_data(): +def get_asset_data(): project_name = get_current_project_name() asset_name = get_current_asset_name() asset_doc = get_asset_by_name(project_name, asset_name) @@ -121,7 +121,7 @@ def _get_asset_data(): return asset_doc.get("data") -def set_start_end_frames(data): +def set_frame_range(data): scene = bpy.context.scene # Default scene settings @@ -171,12 +171,12 @@ def on_new(): set_resolution_startup = settings.get("set_resolution_startup") set_frames_startup = settings.get("set_frames_startup") - data = _get_asset_data() + data = get_asset_data() if set_resolution_startup: set_resolution(data) if set_frames_startup: - set_start_end_frames(data) + set_frame_range(data) unit_scale_settings = settings.get("unit_scale_settings") unit_scale_enabled = unit_scale_settings.get("enabled") @@ -192,12 +192,12 @@ def on_open(): set_resolution_startup = settings.get("set_resolution_startup") set_frames_startup = settings.get("set_frames_startup") - data = _get_asset_data() + data = get_asset_data() if set_resolution_startup: set_resolution(data) if set_frames_startup: - set_start_end_frames(data) + set_frame_range(data) unit_scale_settings = settings.get("unit_scale_settings") unit_scale_enabled = unit_scale_settings.get("enabled") From 503e049dd9a43f828fdc87aee8ecca7b575f7966 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 24 Jul 2023 12:22:53 +0100 Subject: [PATCH 354/446] Fix rig selection sets naming (#539) --- openpype/hosts/maya/plugins/create/create_rig.py | 4 ++-- .../hosts/maya/plugins/publish/validate_rig_output_ids.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 04104cb7cb..345ab6c00d 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -20,6 +20,6 @@ class CreateRig(plugin.MayaCreator): instance_node = instance.get("instance_node") self.log.info("Creating Rig instance set up ...") - controls = cmds.sets(name="controls_SET", empty=True) - pointcache = cmds.sets(name="out_SET", empty=True) + controls = cmds.sets(name=subset_name + "_controls_SET", empty=True) + pointcache = cmds.sets(name=subset_name + "_out_SET", empty=True) cmds.sets([controls, pointcache], forceElement=instance_node) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index 841d005178..cbc750bace 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -47,7 +47,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): invalid = {} if compute: - out_set = next(x for x in instance if x.startswith("out_SET")) + out_set = next(x for x in instance if "out_SET" in x) instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True) instance_nodes = cmds.ls(instance_nodes, long=True) From 1ceda4712a66d85396b868eb46871ad8e98533ee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Jul 2023 14:28:49 +0100 Subject: [PATCH 355/446] Added support for camera in abc extractor --- .../blender/plugins/publish/extract_abc.py | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 1cab9d225b..a2bff0c2f7 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -12,7 +12,7 @@ class ExtractABC(publish.Extractor): label = "Extract ABC" hosts = ["blender"] - families = ["model", "pointcache"] + families = ["model", "pointcache", "camera"] optional = True def process(self, instance): @@ -22,8 +22,6 @@ class ExtractABC(publish.Extractor): filepath = os.path.join(stagingdir, filename) context = bpy.context - scene = context.scene - view_layer = context.view_layer # Perform extraction self.log.info("Performing extraction..") @@ -31,24 +29,46 @@ class ExtractABC(publish.Extractor): plugin.deselect_all() selected = [] - asset_group = None + active = None - for obj in instance: - obj.select_set(True) - selected.append(obj) - if obj.get(AVALON_PROPERTY): - asset_group = obj + flatten = False + + family = instance.data.get("family") + + if family == "camera": + asset_group = None + for obj in instance: + if obj.get(AVALON_PROPERTY): + asset_group = obj + break + assert asset_group, "No asset group found" + + # Need to cast to list because children is a tuple + selected = list(asset_group.children) + active = selected[0] + + for obj in selected: + obj.select_set(True) + + flatten = True + else: + for obj in instance: + obj.select_set(True) + selected.append(obj) + # Set as active the asset group + if obj.get(AVALON_PROPERTY): + active = obj context = plugin.create_blender_context( - active=asset_group, selected=selected) + active=active, selected=selected) - # We export the abc - bpy.ops.wm.alembic_export( - context, - filepath=filepath, - selected=True, - flatten=False - ) + with bpy.context.temp_override(**context): + # We export the abc + bpy.ops.wm.alembic_export( + filepath=filepath, + selected=True, + flatten=flatten + ) plugin.deselect_all() From 3f47ab0fb6456b1858c84d255978dbde2d346c16 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Jul 2023 14:46:10 +0100 Subject: [PATCH 356/446] Moved camera abc extraction to separate class --- .../blender/plugins/publish/extract_abc.py | 37 ++-------- .../plugins/publish/extract_camera_abc.py | 73 +++++++++++++++++++ 2 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 openpype/hosts/blender/plugins/publish/extract_camera_abc.py diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index a2bff0c2f7..f4babc94d3 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -12,7 +12,7 @@ class ExtractABC(publish.Extractor): label = "Extract ABC" hosts = ["blender"] - families = ["model", "pointcache", "camera"] + families = ["model", "pointcache"] optional = True def process(self, instance): @@ -31,33 +31,12 @@ class ExtractABC(publish.Extractor): selected = [] active = None - flatten = False - - family = instance.data.get("family") - - if family == "camera": - asset_group = None - for obj in instance: - if obj.get(AVALON_PROPERTY): - asset_group = obj - break - assert asset_group, "No asset group found" - - # Need to cast to list because children is a tuple - selected = list(asset_group.children) - active = selected[0] - - for obj in selected: - obj.select_set(True) - - flatten = True - else: - for obj in instance: - obj.select_set(True) - selected.append(obj) - # Set as active the asset group - if obj.get(AVALON_PROPERTY): - active = obj + for obj in instance: + obj.select_set(True) + selected.append(obj) + # Set as active the asset group + if obj.get(AVALON_PROPERTY): + active = obj context = plugin.create_blender_context( active=active, selected=selected) @@ -67,7 +46,7 @@ class ExtractABC(publish.Extractor): bpy.ops.wm.alembic_export( filepath=filepath, selected=True, - flatten=flatten + flatten=False ) plugin.deselect_all() diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py new file mode 100644 index 0000000000..a21a59b151 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -0,0 +1,73 @@ +import os + +import bpy + +from openpype.pipeline import publish +from openpype.hosts.blender.api import plugin +from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY + + +class ExtractCameraABC(publish.Extractor): + """Extract camera as ABC.""" + + label = "Extract Camera (ABC)" + hosts = ["blender"] + families = ["camera"] + optional = True + + def process(self, instance): + # Define extract output file path + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.abc" + filepath = os.path.join(stagingdir, filename) + + context = bpy.context + + # Perform extraction + self.log.info("Performing extraction..") + + plugin.deselect_all() + + selected = [] + active = None + + asset_group = None + for obj in instance: + if obj.get(AVALON_PROPERTY): + asset_group = obj + break + assert asset_group, "No asset group found" + + # Need to cast to list because children is a tuple + selected = list(asset_group.children) + active = selected[0] + + for obj in selected: + obj.select_set(True) + + context = plugin.create_blender_context( + active=active, selected=selected) + + with bpy.context.temp_override(**context): + # We export the abc + bpy.ops.wm.alembic_export( + filepath=filepath, + selected=True, + flatten=True + ) + + plugin.deselect_all() + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'abc', + 'ext': 'abc', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) From 7e043501817b3ab6c0619d50875b1f8a4dd04619 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Jul 2023 14:48:04 +0100 Subject: [PATCH 357/446] Added setting for new extractor --- openpype/settings/defaults/project_settings/blender.json | 5 +++++ .../projects_schema/schemas/schema_blender_publish.json | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index fb11e727b3..df865adeba 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -85,6 +85,11 @@ "optional": true, "active": true }, + "ExtractCameraABC": { + "enabled": true, + "optional": true, + "active": true + }, "ExtractLayout": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 1037519f57..d4cafcd62a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -107,6 +107,10 @@ "key": "ExtractCamera", "label": "Extract FBX Camera as FBX" }, + { + "key": "ExtractCameraABC", + "label": "Extract Camera as ABC" + }, { "key": "ExtractLayout", "label": "Extract Layout as JSON" @@ -174,4 +178,4 @@ ] } ] -} \ No newline at end of file +} From 225cbd2ffe3825c3b6ff46c6f31d96f47027b1e2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Jul 2023 14:49:50 +0100 Subject: [PATCH 358/446] Minor changes to FBX camera extractor to improve clarity --- .../publish/{extract_camera.py => extract_camera_fbx.py} | 2 +- .../schemas/projects_schema/schemas/schema_blender_publish.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename openpype/hosts/blender/plugins/publish/{extract_camera.py => extract_camera_fbx.py} (98%) diff --git a/openpype/hosts/blender/plugins/publish/extract_camera.py b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py similarity index 98% rename from openpype/hosts/blender/plugins/publish/extract_camera.py rename to openpype/hosts/blender/plugins/publish/extract_camera_fbx.py index 9fd181825c..315994140e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -9,7 +9,7 @@ from openpype.hosts.blender.api import plugin class ExtractCamera(publish.Extractor): """Extract as the camera as FBX.""" - label = "Extract Camera" + label = "Extract Camera (FBX)" hosts = ["blender"] families = ["camera"] optional = True diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index d4cafcd62a..2f0bf0a831 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -105,7 +105,7 @@ }, { "key": "ExtractCamera", - "label": "Extract FBX Camera as FBX" + "label": "Extract Camera as FBX" }, { "key": "ExtractCameraABC", From 412c83bda1f4e2a3a646c153122b7cead73310dc Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Jul 2023 15:44:23 +0100 Subject: [PATCH 359/446] Set UE_PYTHONPATH when launching Unreal --- openpype/hosts/unreal/addon.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index b5c978d98f..3225d742a3 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -54,7 +54,8 @@ class UnrealAddon(OpenPypeModule, IHostAddon): # Set default environments if are not set via settings defaults = { - "OPENPYPE_LOG_NO_COLORS": "True" + "OPENPYPE_LOG_NO_COLORS": "True", + "UE_PYTHONPATH": os.environ.get("PYTHONPATH", ""), } for key, value in defaults.items(): if not env.get(key): From 58a62a3ccbafce219400efa9cf1a66cd903a769f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 24 Jul 2023 17:45:40 +0200 Subject: [PATCH 360/446] OP-4845 - added settings to limit hardcoded template name 'render' template name was hardcoded which is causing issues in Ayon --- openpype/settings/defaults/project_settings/deadline.json | 1 + .../schemas/projects_schema/schema_project_deadline.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 1b8c8397d7..139a6f44b7 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -101,6 +101,7 @@ }, "ProcessSubmittedJobOnFarm": { "enabled": true, + "template_name": "render", "deadline_department": "", "deadline_pool": "", "deadline_group": "", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 6d59b5a92b..201fca3fa6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -544,6 +544,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "text", + "key": "template_name", + "label": "Publish template name" + }, { "type": "text", "key": "deadline_department", From 2470911c5a8628f33e79a36a2cace8c13b3e4801 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 24 Jul 2023 18:38:07 +0200 Subject: [PATCH 361/446] Revert "OP-4845 - added settings to limit hardcoded template name" This reverts commit 58a62a3ccbafce219400efa9cf1a66cd903a769f. --- openpype/settings/defaults/project_settings/deadline.json | 1 - .../schemas/projects_schema/schema_project_deadline.json | 5 ----- 2 files changed, 6 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 139a6f44b7..1b8c8397d7 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -101,7 +101,6 @@ }, "ProcessSubmittedJobOnFarm": { "enabled": true, - "template_name": "render", "deadline_department": "", "deadline_pool": "", "deadline_group": "", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 201fca3fa6..6d59b5a92b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -544,11 +544,6 @@ "key": "enabled", "label": "Enabled" }, - { - "type": "text", - "key": "template_name", - "label": "Publish template name" - }, { "type": "text", "key": "deadline_department", From 4055536411794dd089c23db8e455e7e93f854434 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 25 Jul 2023 11:48:32 +0100 Subject: [PATCH 362/446] Added env variable to set existing built Ayon plugin --- .../unreal/hooks/pre_workfile_preparation.py | 40 ++++++++++++------- openpype/hosts/unreal/lib.py | 30 ++++++++++++++ 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 760d55077a..e6662e7420 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -187,24 +187,36 @@ class UnrealPrelaunchHook(PreLaunchHook): project_path.mkdir(parents=True, exist_ok=True) - # Set "AYON_UNREAL_PLUGIN" to current process environment for - # execution of `create_unreal_project` - - if self.launch_context.env.get("AYON_UNREAL_PLUGIN"): - self.log.info(( - f"{self.signature} using Ayon plugin from " - f"{self.launch_context.env.get('AYON_UNREAL_PLUGIN')}" - )) - env_key = "AYON_UNREAL_PLUGIN" - if self.launch_context.env.get(env_key): - os.environ[env_key] = self.launch_context.env[env_key] - # engine_path points to the specific Unreal Engine root # so, we are going up from the executable itself 3 levels. engine_path: Path = Path(executable).parents[3] - if not unreal_lib.check_plugin_existence(engine_path): - self.exec_plugin_install(engine_path) + # Check if new env variable exists, and if it does, if the path + # actually contains the plugin. If not, install it. + + built_plugin_path = self.launch_context.env.get( + "AYON_BUILT_UNREAL_PLUGIN", None) + + if unreal_lib.check_built_plugin_existance(built_plugin_path): + self.log.info(( + f"{self.signature} using existing built Ayon plugin from " + f"{built_plugin_path}" + )) + unreal_lib.move_built_plugin(engine_path, Path(built_plugin_path)) + else: + # Set "AYON_UNREAL_PLUGIN" to current process environment for + # execution of `create_unreal_project` + env_key = "AYON_UNREAL_PLUGIN" + if self.launch_context.env.get(env_key): + self.log.info(( + f"{self.signature} using Ayon plugin from " + f"{self.launch_context.env.get(env_key)}" + )) + if self.launch_context.env.get(env_key): + os.environ[env_key] = self.launch_context.env[env_key] + + if not unreal_lib.check_plugin_existence(engine_path): + self.exec_plugin_install(engine_path) project_file = project_path / unreal_project_filename diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 67e7891344..cffb5fd1c0 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -429,6 +429,36 @@ def get_build_id(engine_path: Path, ue_version: str) -> str: return "{" + loaded_modules.get("BuildId") + "}" +def check_built_plugin_existance(plugin_path) -> bool: + if not plugin_path: + return False + + integration_plugin_path = Path(plugin_path) + + if not os.path.isdir(integration_plugin_path): + raise RuntimeError("Path to the integration plugin is null!") + + if not (integration_plugin_path / "Binaries").is_dir() \ + or not (integration_plugin_path / "Intermediate").is_dir(): + return False + + return True + + +def move_built_plugin(engine_path: Path, plugin_path: Path) -> None: + ayon_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" + + if not ayon_plugin_path.is_dir(): + ayon_plugin_path.mkdir(parents=True, exist_ok=True) + + engine_plugin_config_path: Path = ayon_plugin_path / "Config" + engine_plugin_config_path.mkdir(exist_ok=True) + + dir_util._path_created = {} + + dir_util.copy_tree(plugin_path.as_posix(), ayon_plugin_path.as_posix()) + + def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: env = env or os.environ integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) From edbed9ed0e90c5745604a4568109eeab74198efb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 25 Jul 2023 14:43:55 +0100 Subject: [PATCH 363/446] Improved code based on suggestions Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/unreal/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index cffb5fd1c0..5b2e35958b 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -435,7 +435,7 @@ def check_built_plugin_existance(plugin_path) -> bool: integration_plugin_path = Path(plugin_path) - if not os.path.isdir(integration_plugin_path): + if not integration_plugin_path.is_dir(): raise RuntimeError("Path to the integration plugin is null!") if not (integration_plugin_path / "Binaries").is_dir() \ From afbd3d392d4cc9dfa6707192a365e07b4bc0de21 Mon Sep 17 00:00:00 2001 From: Mustafa Zarkash Date: Tue, 25 Jul 2023 16:43:59 +0300 Subject: [PATCH 364/446] Fix colorspace compatibility check (#5334) * update compatibility_check * update doc-string --- openpype/pipeline/colorspace.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 3f2d4891c1..caa0f6dcd7 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -237,10 +237,17 @@ def get_data_subprocess(config_path, data_type): def compatibility_check(): - """Making sure PyOpenColorIO is importable""" + """checking if user has a compatible PyOpenColorIO >= 2. + + It's achieved by checking if PyOpenColorIO is importable + and calling any version 2 specific function + """ try: - import PyOpenColorIO # noqa: F401 - except ImportError: + import PyOpenColorIO + + # ocio versions lower than 2 will raise AttributeError + PyOpenColorIO.GetVersion() + except (ImportError, AttributeError): return False return True From 7fd99e59a3f996e838f1c2fc231fd22b9077bb28 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 25 Jul 2023 14:08:35 +0000 Subject: [PATCH 365/446] [Automated] Release --- CHANGELOG.md | 180 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 182 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07b95c7343..f2930d45eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,186 @@ # Changelog +## [3.16.2](https://github.com/ynput/OpenPype/tree/3.16.2) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.1...3.16.2) + +### **🆕 New features** + + +
+Fusion - Set selected tool to active #5327 + +When you run the action to select a node, this PR makes the node-flow show the selected node + you'll see the nodes controls in the inspector. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya: All base create plugins #5326 + +Prepared base classes for each creator type in Maya. Extended `MayaCreatorBase` to have default implementations of common logic with instances which is used in each type of plugin. + + +___ + +
+ + +
+Windows: Support long paths on zip updates. #5265 + +Support long paths for version extract on Windows.Use case is when having long paths in for example an addon. You can install to the C drive but because the zip files are extracted in the local users folder, it'll add additional sub directories to the paths and quickly get too long paths for Windows to handle the zip updates. + + +___ + +
+ + +
+Blender: Added setting to set resolution and start/end frames at startup #5338 + +This PR adds `set_resolution_startup`and `set_frames_startup` settings. They automatically set respectively the resolution and start/end frames and FPS in Blender when opening a file or creating a new one. + + +___ + +
+ + +
+Blender: Support for ExtractBurnin #5339 + +This PR adds support for ExtractBurnin for Blender, when publishing a Review. + + +___ + +
+ + +
+Blender: Extract Camera as Alembic #5343 + +Added support to extract Alembic Cameras in Blender. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Validate Instance In Context #5335 + +Missing new publisher error so the repair action shows up. + + +___ + +
+ + +
+Settings: Fix default settings #5311 + +Fixed defautl settings for shotgrid. Renamed `FarmRootEnumEntity` to `DynamicEnumEntity` and removed doubled ABC metaclass definition (all settings entities have abstract metaclass). + + +___ + +
+ + +
+Deadline: missing context argument #5312 + +Updated function arguments + + +___ + +
+ + +
+Qt UI: Multiselection combobox PySide6 compatibility #5314 + +- The check states are replaced with the values for PySide6 +- `QtCore.Qt.ItemIsUserTristate` is used instead of `QtCore.Qt.ItemIsTristate` to avoid crashes on PySide6 + + +___ + +
+ + +
+Docker: handle openssl 1.1.1 for centos 7 docker build #5319 + +Move to python 3.9 has added need to use openssl 1.1.x - but it is not by default available on centos 7 image. This is fixing it. + + +___ + +
+ + +
+houdini: fix typo in redshift proxy #5320 + +I believe there's a typo in `create_redshift_proxy.py` ( extra ` ) in filename, and I made this PR to suggest a fix + + +___ + +
+ + +
+Houdini: fix wrong creator identifier in pointCache workflow #5324 + +FIxing a bug in publishing alembics, were invalid creator identifier caused missing family association. + + +___ + +
+ + +
+Fix colorspace compatibility check #5334 + +for some reason a user may have `PyOpenColorIO` installed to his machine, _in my case it came with renderman._it can trick the compatibility check as `import PyOpenColorIO` won't raise an error however it may be an old version _like my case_Beforecompatibility check was true and It used wrapper directly After Fix It will use wrapper via subprocess instead + + +___ + +
+ +### **Merged pull requests** + + +
+Remove forgotten dev logging #5315 + + +___ + +
+ + + + ## [3.16.1](https://github.com/ynput/OpenPype/tree/3.16.1) diff --git a/openpype/version.py b/openpype/version.py index e46b97c063..9a4fef421c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.2-nightly.2" +__version__ = "3.16.2" diff --git a/pyproject.toml b/pyproject.toml index fb6e222f27..c4596a7edd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.1" # OpenPype +version = "3.16.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From cc1e522dfbd958f43bece0e04759d332369d57a5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 25 Jul 2023 14:09:32 +0000 Subject: [PATCH 366/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 66db18026d..e7717f395f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.2 - 3.16.2-nightly.2 - 3.16.2-nightly.1 - 3.16.1 @@ -134,7 +135,6 @@ body: - 3.14.7-nightly.1 - 3.14.6 - 3.14.6-nightly.3 - - 3.14.6-nightly.2 validations: required: true - type: dropdown From f0801cb098c05eee2e9a177752ad3cac6f08da4b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:19:14 +0200 Subject: [PATCH 367/446] Chore: Remove deprecated functions (#5323) * removed deprecated files from root of openpype * removed deprecated content from openype/lib * fix imports --- openpype/action.py | 135 ---- openpype/lib/__init__.py | 63 +- openpype/lib/avalon_context.py | 654 ------------------ openpype/lib/delivery.py | 252 ------- openpype/lib/execute.py | 12 - openpype/lib/log.py | 18 - openpype/lib/mongo.py | 61 -- openpype/lib/path_tools.py | 143 ---- openpype/lib/plugin_tools.py | 148 ---- .../publish/collect_shotgrid_entities.py | 4 +- openpype/plugin.py | 128 ---- openpype/settings/handlers.py | 5 +- 12 files changed, 3 insertions(+), 1620 deletions(-) delete mode 100644 openpype/action.py delete mode 100644 openpype/lib/avalon_context.py delete mode 100644 openpype/lib/delivery.py delete mode 100644 openpype/lib/mongo.py delete mode 100644 openpype/plugin.py diff --git a/openpype/action.py b/openpype/action.py deleted file mode 100644 index 6114c65fd4..0000000000 --- a/openpype/action.py +++ /dev/null @@ -1,135 +0,0 @@ -import warnings -import functools -import pyblish.api - - -class ActionDeprecatedWarning(DeprecationWarning): - pass - - -def deprecated(new_destination): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - func = None - if callable(new_destination): - func = new_destination - new_destination = None - - def _decorator(decorated_func): - if new_destination is None: - warning_message = ( - " Please check content of deprecated function to figure out" - " possible replacement." - ) - else: - warning_message = " Please replace your usage with '{}'.".format( - new_destination - ) - - @functools.wraps(decorated_func) - def wrapper(*args, **kwargs): - warnings.simplefilter("always", ActionDeprecatedWarning) - warnings.warn( - ( - "Call to deprecated function '{}'" - "\nFunction was moved or removed.{}" - ).format(decorated_func.__name__, warning_message), - category=ActionDeprecatedWarning, - stacklevel=4 - ) - return decorated_func(*args, **kwargs) - return wrapper - - if func is None: - return _decorator - return _decorator(func) - - -@deprecated("openpype.pipeline.publish.get_errored_instances_from_context") -def get_errored_instances_from_context(context, plugin=None): - """ - Deprecated: - Since 3.14.* will be removed in 3.16.* or later. - """ - - from openpype.pipeline.publish import get_errored_instances_from_context - - return get_errored_instances_from_context(context, plugin=plugin) - - -@deprecated("openpype.pipeline.publish.get_errored_plugins_from_context") -def get_errored_plugins_from_data(context): - """ - Deprecated: - Since 3.14.* will be removed in 3.16.* or later. - """ - - from openpype.pipeline.publish import get_errored_plugins_from_context - - return get_errored_plugins_from_context(context) - - -class RepairAction(pyblish.api.Action): - """Repairs the action - - To process the repairing this requires a static `repair(instance)` method - is available on the plugin. - - Deprecated: - 'RepairAction' and 'RepairContextAction' were moved to - 'openpype.pipeline.publish' please change you imports. - There is no "reasonable" way hot mark these classes as deprecated - to show warning of wrong import. Deprecated since 3.14.* will be - removed in 3.16.* - - """ - label = "Repair" - on = "failed" # This action is only available on a failed plug-in - icon = "wrench" # Icon from Awesome Icon - - def process(self, context, plugin): - - if not hasattr(plugin, "repair"): - raise RuntimeError("Plug-in does not have repair method.") - - # Get the errored instances - self.log.info("Finding failed instances..") - errored_instances = get_errored_instances_from_context(context, - plugin=plugin) - for instance in errored_instances: - plugin.repair(instance) - - -class RepairContextAction(pyblish.api.Action): - """Repairs the action - - To process the repairing this requires a static `repair(instance)` method - is available on the plugin. - - Deprecated: - 'RepairAction' and 'RepairContextAction' were moved to - 'openpype.pipeline.publish' please change you imports. - There is no "reasonable" way hot mark these classes as deprecated - to show warning of wrong import. Deprecated since 3.14.* will be - removed in 3.16.* - - """ - label = "Repair" - on = "failed" # This action is only available on a failed plug-in - - def process(self, context, plugin): - - if not hasattr(plugin, "repair"): - raise RuntimeError("Plug-in does not have repair method.") - - # Get the errored instances - self.log.info("Finding failed instances..") - errored_plugins = get_errored_plugins_from_data(context) - - # Apply pyblish.logic to get the instances for the plug-in - if plugin in errored_plugins: - self.log.info("Attempting fix ...") - plugin.repair(context) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 06de486f2e..9065588cf1 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -53,7 +53,6 @@ from .env_tools import ( from .terminal import Terminal from .execute import ( get_openpype_execute_args, - get_pype_execute_args, get_linux_launcher_args, execute, run_subprocess, @@ -65,7 +64,6 @@ from .execute import ( ) from .log import ( Logger, - PypeLogger, ) from .path_templates import ( @@ -77,12 +75,6 @@ from .path_templates import ( FormatObject, ) -from .mongo import ( - get_default_components, - validate_mongo_connection, - OpenPypeMongoConnection -) - from .dateutils import ( get_datetime_data, get_timestamp, @@ -115,25 +107,6 @@ from .transcoding import ( convert_ffprobe_fps_value, convert_ffprobe_fps_to_float, ) -from .avalon_context import ( - CURRENT_DOC_SCHEMAS, - create_project, - - get_workfile_template_key, - get_workfile_template_key_from_context, - get_last_workfile_with_version, - get_last_workfile, - - BuildWorkfile, - - get_creator_by_name, - - get_custom_workfile_template, - - get_custom_workfile_template_by_context, - get_custom_workfile_template_by_string_context, - get_custom_workfile_template -) from .local_settings import ( IniSettingRegistry, @@ -163,9 +136,6 @@ from .applications import ( ) from .plugin_tools import ( - TaskNotSetError, - get_subset_name, - get_subset_name_with_asset_doc, prepare_template_data, source_hash, ) @@ -177,9 +147,6 @@ from .path_tools import ( version_up, get_version_from_path, get_last_version_from_path, - create_project_folders, - create_workdir_extra_folders, - get_project_basic_paths, ) from .openpype_version import ( @@ -207,7 +174,6 @@ __all__ = [ "find_executable", "get_openpype_execute_args", - "get_pype_execute_args", "get_linux_launcher_args", "execute", "run_subprocess", @@ -257,22 +223,6 @@ __all__ = [ "convert_ffprobe_fps_value", "convert_ffprobe_fps_to_float", - "CURRENT_DOC_SCHEMAS", - "create_project", - - "get_workfile_template_key", - "get_workfile_template_key_from_context", - "get_last_workfile_with_version", - "get_last_workfile", - - "BuildWorkfile", - - "get_creator_by_name", - - "get_custom_workfile_template_by_context", - "get_custom_workfile_template_by_string_context", - "get_custom_workfile_template", - "IniSettingRegistry", "JSONSettingRegistry", "OpenPypeSecureRegistry", @@ -298,9 +248,7 @@ __all__ = [ "filter_profiles", - "TaskNotSetError", - "get_subset_name", - "get_subset_name_with_asset_doc", + "prepare_template_data", "source_hash", "format_file_size", @@ -323,15 +271,6 @@ __all__ = [ "get_formatted_current_time", "Logger", - "PypeLogger", - - "get_default_components", - "validate_mongo_connection", - "OpenPypeMongoConnection", - - "create_project_folders", - "create_workdir_extra_folders", - "get_project_basic_paths", "op_version_control_available", "get_openpype_version", diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py deleted file mode 100644 index a9ae27cb79..0000000000 --- a/openpype/lib/avalon_context.py +++ /dev/null @@ -1,654 +0,0 @@ -"""Should be used only inside of hosts.""" - -import platform -import logging -import functools -import warnings - -import six - -from openpype.client import ( - get_project, - get_asset_by_name, -) -from openpype.client.operations import ( - CURRENT_ASSET_DOC_SCHEMA, - CURRENT_PROJECT_SCHEMA, - CURRENT_PROJECT_CONFIG_SCHEMA, -) -from .profiles_filtering import filter_profiles -from .path_templates import StringTemplate - -legacy_io = None - -log = logging.getLogger("AvalonContext") - - -# Backwards compatibility - should not be used anymore -# - Will be removed in OP 3.16.* -CURRENT_DOC_SCHEMAS = { - "project": CURRENT_PROJECT_SCHEMA, - "asset": CURRENT_ASSET_DOC_SCHEMA, - "config": CURRENT_PROJECT_CONFIG_SCHEMA -} - - -class AvalonContextDeprecatedWarning(DeprecationWarning): - pass - - -def deprecated(new_destination): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - func = None - if callable(new_destination): - func = new_destination - new_destination = None - - def _decorator(decorated_func): - if new_destination is None: - warning_message = ( - " Please check content of deprecated function to figure out" - " possible replacement." - ) - else: - warning_message = " Please replace your usage with '{}'.".format( - new_destination - ) - - @functools.wraps(decorated_func) - def wrapper(*args, **kwargs): - warnings.simplefilter("always", AvalonContextDeprecatedWarning) - warnings.warn( - ( - "Call to deprecated function '{}'" - "\nFunction was moved or removed.{}" - ).format(decorated_func.__name__, warning_message), - category=AvalonContextDeprecatedWarning, - stacklevel=4 - ) - return decorated_func(*args, **kwargs) - return wrapper - - if func is None: - return _decorator - return _decorator(func) - - -@deprecated("openpype.client.operations.create_project") -def create_project( - project_name, project_code, library_project=False, dbcon=None -): - """Create project using OpenPype settings. - - This project creation function is not validating project document on - creation. It is because project document is created blindly with only - minimum required information about project which is it's name, code, type - and schema. - - Entered project name must be unique and project must not exist yet. - - Args: - project_name(str): New project name. Should be unique. - project_code(str): Project's code should be unique too. - library_project(bool): Project is library project. - dbcon(AvalonMongoDB): Object of connection to MongoDB. - - Raises: - ValueError: When project name already exists in MongoDB. - - Returns: - dict: Created project document. - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.client.operations import create_project - - return create_project(project_name, project_code, library_project) - - -def with_pipeline_io(func): - @functools.wraps(func) - def wrapped(*args, **kwargs): - global legacy_io - if legacy_io is None: - from openpype.pipeline import legacy_io - return func(*args, **kwargs) - return wrapped - - -@deprecated("openpype.client.get_linked_asset_ids") -def get_linked_asset_ids(asset_doc): - """Return linked asset ids for `asset_doc` from DB - - Args: - asset_doc (dict): Asset document from DB. - - Returns: - (list): MongoDB ids of input links. - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.client import get_linked_asset_ids - from openpype.pipeline import legacy_io - - project_name = legacy_io.active_project() - - return get_linked_asset_ids(project_name, asset_doc=asset_doc) - - -@deprecated( - "openpype.pipeline.workfile.get_workfile_template_key_from_context") -def get_workfile_template_key_from_context( - asset_name, task_name, host_name, project_name=None, - dbcon=None, project_settings=None -): - """Helper function to get template key for workfile template. - - Do the same as `get_workfile_template_key` but returns value for "session - context". - - It is required to pass one of 'dbcon' with already set project name or - 'project_name' arguments. - - Args: - asset_name(str): Name of asset document. - task_name(str): Task name for which is template key retrieved. - Must be available on asset document under `data.tasks`. - host_name(str): Name of host implementation for which is workfile - used. - project_name(str): Project name where asset and task is. Not required - when 'dbcon' is passed. - dbcon(AvalonMongoDB): Connection to mongo with already set project - under `AVALON_PROJECT`. Not required when 'project_name' is passed. - project_settings(dict): Project settings for passed 'project_name'. - Not required at all but makes function faster. - Raises: - ValueError: When both 'dbcon' and 'project_name' were not - passed. - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.workfile import ( - get_workfile_template_key_from_context - ) - - if not project_name: - if not dbcon: - raise ValueError(( - "`get_workfile_template_key_from_context` requires to pass" - " one of 'dbcon' or 'project_name' arguments." - )) - project_name = dbcon.active_project() - - return get_workfile_template_key_from_context( - asset_name, task_name, host_name, project_name, project_settings - ) - - -@deprecated( - "openpype.pipeline.workfile.get_workfile_template_key") -def get_workfile_template_key( - task_type, host_name, project_name=None, project_settings=None -): - """Workfile template key which should be used to get workfile template. - - Function is using profiles from project settings to return right template - for passet task type and host name. - - One of 'project_name' or 'project_settings' must be passed it is preferred - to pass settings if are already available. - - Args: - task_type(str): Name of task type. - host_name(str): Name of host implementation (e.g. "maya", "nuke", ...) - project_name(str): Name of project in which context should look for - settings. Not required if `project_settings` are passed. - project_settings(dict): Prepare project settings for project name. - Not needed if `project_name` is passed. - - Raises: - ValueError: When both 'project_name' and 'project_settings' were not - passed. - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.workfile import get_workfile_template_key - - return get_workfile_template_key( - task_type, host_name, project_name, project_settings - ) - - -@deprecated("openpype.pipeline.context_tools.compute_session_changes") -def compute_session_changes( - session, task=None, asset=None, app=None, template_key=None -): - """Compute the changes for a Session object on asset, task or app switch - - This does *NOT* update the Session object, but returns the changes - required for a valid update of the Session. - - Args: - session (dict): The initial session to compute changes to. - This is required for computing the full Work Directory, as that - also depends on the values that haven't changed. - task (str, Optional): Name of task to switch to. - asset (str or dict, Optional): Name of asset to switch to. - You can also directly provide the Asset dictionary as returned - from the database to avoid an additional query. (optimization) - app (str, Optional): Name of app to switch to. - - Returns: - dict: The required changes in the Session dictionary. - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline import legacy_io - from openpype.pipeline.context_tools import compute_session_changes - - if isinstance(asset, six.string_types): - project_name = legacy_io.active_project() - asset = get_asset_by_name(project_name, asset) - - return compute_session_changes( - session, - asset, - task, - template_key - ) - - -@deprecated("openpype.pipeline.context_tools.get_workdir_from_session") -def get_workdir_from_session(session=None, template_key=None): - """Calculate workdir path based on session data. - - Args: - session (Union[None, Dict[str, str]]): Session to use. If not passed - current context session is used (from legacy_io). - template_key (Union[str, None]): Precalculate template key to define - workfile template name in Anatomy. - - Returns: - str: Workdir path. - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.context_tools import get_workdir_from_session - - return get_workdir_from_session(session, template_key) - - -@deprecated("openpype.pipeline.context_tools.change_current_context") -def update_current_task(task=None, asset=None, app=None, template_key=None): - """Update active Session to a new task work area. - - This updates the live Session to a different `asset`, `task` or `app`. - - Args: - task (str): The task to set. - asset (str): The asset to set. - app (str): The app to set. - - Returns: - dict: The changed key, values in the current Session. - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline import legacy_io - from openpype.pipeline.context_tools import change_current_context - - project_name = legacy_io.active_project() - if isinstance(asset, six.string_types): - asset = get_asset_by_name(project_name, asset) - - return change_current_context(asset, task, template_key) - - -@deprecated("openpype.pipeline.workfile.BuildWorkfile") -def BuildWorkfile(): - """Build workfile class was moved to workfile pipeline. - - Deprecated: - Function will be removed after release version 3.16.* - """ - from openpype.pipeline.workfile import BuildWorkfile - - return BuildWorkfile() - - -@deprecated("openpype.pipeline.create.get_legacy_creator_by_name") -def get_creator_by_name(creator_name, case_sensitive=False): - """Find creator plugin by name. - - Args: - creator_name (str): Name of creator class that should be returned. - case_sensitive (bool): Match of creator plugin name is case sensitive. - Set to `False` by default. - - Returns: - Creator: Return first matching plugin or `None`. - - Deprecated: - Function will be removed after release version 3.16.* - """ - from openpype.pipeline.create import get_legacy_creator_by_name - - return get_legacy_creator_by_name(creator_name, case_sensitive) - - -def _get_task_context_data_for_anatomy( - project_doc, asset_doc, task_name, anatomy=None -): - """Prepare Task context for anatomy data. - - WARNING: this data structure is currently used only in workfile templates. - Key "task" is currently in rest of pipeline used as string with task - name. - - Args: - project_doc (dict): Project document with available "name" and - "data.code" keys. - asset_doc (dict): Asset document from MongoDB. - task_name (str): Name of context task. - anatomy (Anatomy): Optionally Anatomy for passed project name can be - passed as Anatomy creation may be slow. - - Returns: - dict: With Anatomy context data. - """ - - from openpype.pipeline.template_data import get_general_template_data - - if anatomy is None: - from openpype.pipeline import Anatomy - anatomy = Anatomy(project_doc["name"]) - - asset_name = asset_doc["name"] - project_task_types = anatomy["tasks"] - - # get relevant task type from asset doc - assert task_name in asset_doc["data"]["tasks"], ( - "Task name \"{}\" not found on asset \"{}\"".format( - task_name, asset_name - ) - ) - - task_type = asset_doc["data"]["tasks"][task_name].get("type") - - assert task_type, ( - "Task name \"{}\" on asset \"{}\" does not have specified task type." - ).format(asset_name, task_name) - - # get short name for task type defined in default anatomy settings - project_task_type_data = project_task_types.get(task_type) - assert project_task_type_data, ( - "Something went wrong. Default anatomy tasks are not holding" - "requested task type: `{}`".format(task_type) - ) - - data = { - "project": { - "name": project_doc["name"], - "code": project_doc["data"].get("code") - }, - "asset": asset_name, - "task": { - "name": task_name, - "type": task_type, - "short": project_task_type_data["short_name"] - } - } - - system_general_data = get_general_template_data() - data.update(system_general_data) - - return data - - -@deprecated( - "openpype.pipeline.workfile.get_custom_workfile_template_by_context") -def get_custom_workfile_template_by_context( - template_profiles, project_doc, asset_doc, task_name, anatomy=None -): - """Filter and fill workfile template profiles by passed context. - - It is expected that passed argument are already queried documents of - project and asset as parents of processing task name. - - Existence of formatted path is not validated. - - Args: - template_profiles(list): Template profiles from settings. - project_doc(dict): Project document from MongoDB. - asset_doc(dict): Asset document from MongoDB. - task_name(str): Name of task for which templates are filtered. - anatomy(Anatomy): Optionally passed anatomy object for passed project - name. - - Returns: - str: Path to template or None if none of profiles match current - context. (Existence of formatted path is not validated.) - - Deprecated: - Function will be removed after release version 3.16.* - """ - - if anatomy is None: - from openpype.pipeline import Anatomy - anatomy = Anatomy(project_doc["name"]) - - # get project, asset, task anatomy context data - anatomy_context_data = _get_task_context_data_for_anatomy( - project_doc, asset_doc, task_name, anatomy - ) - # add root dict - anatomy_context_data["root"] = anatomy.roots - - # get task type for the task in context - current_task_type = anatomy_context_data["task"]["type"] - - # get path from matching profile - matching_item = filter_profiles( - template_profiles, - {"task_types": current_task_type} - ) - # when path is available try to format it in case - # there are some anatomy template strings - if matching_item: - template = matching_item["path"][platform.system().lower()] - return StringTemplate.format_strict_template( - template, anatomy_context_data - ) - - return None - - -@deprecated( - "openpype.pipeline.workfile.get_custom_workfile_template_by_string_context" -) -def get_custom_workfile_template_by_string_context( - template_profiles, project_name, asset_name, task_name, - dbcon=None, anatomy=None -): - """Filter and fill workfile template profiles by passed context. - - Passed context are string representations of project, asset and task. - Function will query documents of project and asset to be able use - `get_custom_workfile_template_by_context` for rest of logic. - - Args: - template_profiles(list): Loaded workfile template profiles. - project_name(str): Project name. - asset_name(str): Asset name. - task_name(str): Task name. - dbcon(AvalonMongoDB): Optional avalon implementation of mongo - connection with context Session. - anatomy(Anatomy): Optionally prepared anatomy object for passed - project. - - Returns: - str: Path to template or None if none of profiles match current - context. (Existence of formatted path is not validated.) - - Deprecated: - Function will be removed after release version 3.16.* - """ - - project_name = None - if anatomy is not None: - project_name = anatomy.project_name - - if not project_name and dbcon is not None: - project_name = dbcon.active_project() - - if not project_name: - raise ValueError("Can't determina project") - - project_doc = get_project(project_name, fields=["name", "data.code"]) - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["name", "data.tasks"]) - - return get_custom_workfile_template_by_context( - template_profiles, project_doc, asset_doc, task_name, anatomy - ) - - -@deprecated("openpype.pipeline.context_tools.get_custom_workfile_template") -def get_custom_workfile_template(template_profiles): - """Filter and fill workfile template profiles by current context. - - Current context is defined by `legacy_io.Session`. That's why this - function should be used only inside host where context is set and stable. - - Args: - template_profiles(list): Template profiles from settings. - - Returns: - str: Path to template or None if none of profiles match current - context. (Existence of formatted path is not validated.) - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline import legacy_io - - return get_custom_workfile_template_by_string_context( - template_profiles, - legacy_io.Session["AVALON_PROJECT"], - legacy_io.Session["AVALON_ASSET"], - legacy_io.Session["AVALON_TASK"], - legacy_io - ) - - -@deprecated("openpype.pipeline.workfile.get_last_workfile_with_version") -def get_last_workfile_with_version( - workdir, file_template, fill_data, extensions -): - """Return last workfile version. - - Args: - workdir(str): Path to dir where workfiles are stored. - file_template(str): Template of file name. - fill_data(dict): Data for filling template. - extensions(list, tuple): All allowed file extensions of workfile. - - Returns: - tuple: Last workfile with version if there is any otherwise - returns (None, None). - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.workfile import get_last_workfile_with_version - - return get_last_workfile_with_version( - workdir, file_template, fill_data, extensions - ) - - -@deprecated("openpype.pipeline.workfile.get_last_workfile") -def get_last_workfile( - workdir, file_template, fill_data, extensions, full_path=False -): - """Return last workfile filename. - - Returns file with version 1 if there is not workfile yet. - - Args: - workdir(str): Path to dir where workfiles are stored. - file_template(str): Template of file name. - fill_data(dict): Data for filling template. - extensions(list, tuple): All allowed file extensions of workfile. - full_path(bool): Full path to file is returned if set to True. - - Returns: - str: Last or first workfile as filename of full path to filename. - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.workfile import get_last_workfile - - return get_last_workfile( - workdir, file_template, fill_data, extensions, full_path - ) - - -@deprecated("openpype.client.get_linked_representation_id") -def get_linked_ids_for_representations( - project_name, repre_ids, dbcon=None, link_type=None, max_depth=0 -): - """Returns list of linked ids of particular type (if provided). - - Goes from representations to version, back to representations - Args: - project_name (str) - repre_ids (list) or (ObjectId) - dbcon (avalon.mongodb.AvalonMongoDB, optional): Avalon Mongo connection - with Session. - link_type (str): ['reference', '..] - max_depth (int): limit how many levels of recursion - - Returns: - (list) of ObjectId - linked representations - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.client import get_linked_representation_id - - if not isinstance(repre_ids, list): - repre_ids = [repre_ids] - - output = [] - for repre_id in repre_ids: - output.extend(get_linked_representation_id( - project_name, - repre_id=repre_id, - link_type=link_type, - max_depth=max_depth - )) - return output diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py deleted file mode 100644 index efb542de75..0000000000 --- a/openpype/lib/delivery.py +++ /dev/null @@ -1,252 +0,0 @@ -"""Functions useful for delivery action or loader""" -import os -import shutil -import functools -import warnings - - -class DeliveryDeprecatedWarning(DeprecationWarning): - pass - - -def deprecated(new_destination): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - func = None - if callable(new_destination): - func = new_destination - new_destination = None - - def _decorator(decorated_func): - if new_destination is None: - warning_message = ( - " Please check content of deprecated function to figure out" - " possible replacement." - ) - else: - warning_message = " Please replace your usage with '{}'.".format( - new_destination - ) - - @functools.wraps(decorated_func) - def wrapper(*args, **kwargs): - warnings.simplefilter("always", DeliveryDeprecatedWarning) - warnings.warn( - ( - "Call to deprecated function '{}'" - "\nFunction was moved or removed.{}" - ).format(decorated_func.__name__, warning_message), - category=DeliveryDeprecatedWarning, - stacklevel=4 - ) - return decorated_func(*args, **kwargs) - return wrapper - - if func is None: - return _decorator - return _decorator(func) - - -@deprecated("openpype.lib.path_tools.collect_frames") -def collect_frames(files): - """Returns dict of source path and its frame, if from sequence - - Uses clique as most precise solution, used when anatomy template that - created files is not known. - - Assumption is that frames are separated by '.', negative frames are not - allowed. - - Args: - files(list) or (set with single value): list of source paths - - Returns: - (dict): {'/asset/subset_v001.0001.png': '0001', ....} - - Deprecated: - Function was moved to different location and will be removed - after 3.16.* release. - """ - - from .path_tools import collect_frames - - return collect_frames(files) - - -@deprecated("openpype.lib.path_tools.format_file_size") -def sizeof_fmt(num, suffix=None): - """Returns formatted string with size in appropriate unit - - Deprecated: - Function was moved to different location and will be removed - after 3.16.* release. - """ - - from .path_tools import format_file_size - return format_file_size(num, suffix) - - -@deprecated("openpype.pipeline.load.get_representation_path_with_anatomy") -def path_from_representation(representation, anatomy): - """Get representation path using representation document and anatomy. - - Args: - representation (Dict[str, Any]): Representation document. - anatomy (Anatomy): Project anatomy. - - Deprecated: - Function was moved to different location and will be removed - after 3.16.* release. - """ - - from openpype.pipeline.load import get_representation_path_with_anatomy - - return get_representation_path_with_anatomy(representation, anatomy) - - -@deprecated -def copy_file(src_path, dst_path): - """Hardlink file if possible(to save space), copy if not""" - from openpype.lib import create_hard_link # safer importing - - if os.path.exists(dst_path): - return - try: - create_hard_link( - src_path, - dst_path - ) - except OSError: - shutil.copyfile(src_path, dst_path) - - -@deprecated("openpype.pipeline.delivery.get_format_dict") -def get_format_dict(anatomy, location_path): - """Returns replaced root values from user provider value. - - Args: - anatomy (Anatomy) - location_path (str): user provided value - - Returns: - (dict): prepared for formatting of a template - - Deprecated: - Function was moved to different location and will be removed - after 3.16.* release. - """ - - from openpype.pipeline.delivery import get_format_dict - - return get_format_dict(anatomy, location_path) - - -@deprecated("openpype.pipeline.delivery.check_destination_path") -def check_destination_path(repre_id, - anatomy, anatomy_data, - datetime_data, template_name): - """ Try to create destination path based on 'template_name'. - - In the case that path cannot be filled, template contains unmatched - keys, provide error message to filter out repre later. - - Args: - anatomy (Anatomy) - anatomy_data (dict): context to fill anatomy - datetime_data (dict): values with actual date - template_name (str): to pick correct delivery template - - Returns: - (collections.defauldict): {"TYPE_OF_ERROR":"ERROR_DETAIL"} - - Deprecated: - Function was moved to different location and will be removed - after 3.16.* release. - """ - - from openpype.pipeline.delivery import check_destination_path - - return check_destination_path( - repre_id, - anatomy, - anatomy_data, - datetime_data, - template_name - ) - - -@deprecated("openpype.pipeline.delivery.deliver_single_file") -def process_single_file( - src_path, repre, anatomy, template_name, anatomy_data, format_dict, - report_items, log -): - """Copy single file to calculated path based on template - - Args: - src_path(str): path of source representation file - _repre (dict): full repre, used only in process_sequence, here only - as to share same signature - anatomy (Anatomy) - template_name (string): user selected delivery template name - anatomy_data (dict): data from repre to fill anatomy with - format_dict (dict): root dictionary with names and values - report_items (collections.defaultdict): to return error messages - log (Logger): for log printing - - Returns: - (collections.defaultdict , int) - - Deprecated: - Function was moved to different location and will be removed - after 3.16.* release. - """ - - from openpype.pipeline.delivery import deliver_single_file - - return deliver_single_file( - src_path, repre, anatomy, template_name, anatomy_data, format_dict, - report_items, log - ) - - -@deprecated("openpype.pipeline.delivery.deliver_sequence") -def process_sequence( - src_path, repre, anatomy, template_name, anatomy_data, format_dict, - report_items, log -): - """ For Pype2(mainly - works in 3 too) where representation might not - contain files. - - Uses listing physical files (not 'files' on repre as a)might not be - present, b)might not be reliable for representation and copying them. - - TODO Should be refactored when files are sufficient to drive all - representations. - - Args: - src_path(str): path of source representation file - repre (dict): full representation - anatomy (Anatomy) - template_name (string): user selected delivery template name - anatomy_data (dict): data from repre to fill anatomy with - format_dict (dict): root dictionary with names and values - report_items (collections.defaultdict): to return error messages - log (Logger): for log printing - - Returns: - (collections.defaultdict , int) - - Deprecated: - Function was moved to different location and will be removed - after 3.16.* release. - """ - - from openpype.pipeline.delivery import deliver_sequence - - return deliver_sequence( - src_path, repre, anatomy, template_name, anatomy_data, format_dict, - report_items, log - ) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 6c1425fc63..b3c8185d3e 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -296,18 +296,6 @@ def path_to_subprocess_arg(path): return subprocess.list2cmdline([path]) -def get_pype_execute_args(*args): - """Backwards compatible function for 'get_openpype_execute_args'.""" - import traceback - - log = Logger.get_logger("get_pype_execute_args") - stack = "\n".join(traceback.format_stack()) - log.warning(( - "Using deprecated function 'get_pype_execute_args'. Called from:\n{}" - ).format(stack)) - return get_openpype_execute_args(*args) - - def get_openpype_execute_args(*args): """Arguments to run pype command. diff --git a/openpype/lib/log.py b/openpype/lib/log.py index dc2e6615fe..72071063ec 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -492,21 +492,3 @@ class Logger: cls.initialize() return OpenPypeMongoConnection.get_mongo_client() - - -class PypeLogger(Logger): - """Duplicate of 'Logger'. - - Deprecated: - Class will be removed after release version 3.16.* - """ - - @classmethod - def get_logger(cls, *args, **kwargs): - logger = Logger.get_logger(*args, **kwargs) - # TODO uncomment when replaced most of places - logger.warning(( - "'openpype.lib.PypeLogger' is deprecated class." - " Please use 'openpype.lib.Logger' instead." - )) - return logger diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py deleted file mode 100644 index bb2ee6016a..0000000000 --- a/openpype/lib/mongo.py +++ /dev/null @@ -1,61 +0,0 @@ -import warnings -import functools -from openpype.client.mongo import ( - MongoEnvNotSet, - OpenPypeMongoConnection, -) - - -class MongoDeprecatedWarning(DeprecationWarning): - pass - - -def mongo_deprecated(func): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - @functools.wraps(func) - def new_func(*args, **kwargs): - warnings.simplefilter("always", MongoDeprecatedWarning) - warnings.warn( - ( - "Call to deprecated function '{}'." - " Function was moved to 'openpype.client.mongo'." - ).format(func.__name__), - category=MongoDeprecatedWarning, - stacklevel=2 - ) - return func(*args, **kwargs) - return new_func - - -@mongo_deprecated -def get_default_components(): - from openpype.client.mongo import get_default_components - - return get_default_components() - - -@mongo_deprecated -def should_add_certificate_path_to_mongo_url(mongo_url): - from openpype.client.mongo import should_add_certificate_path_to_mongo_url - - return should_add_certificate_path_to_mongo_url(mongo_url) - - -@mongo_deprecated -def validate_mongo_connection(mongo_uri): - from openpype.client.mongo import validate_mongo_connection - - return validate_mongo_connection(mongo_uri) - - -__all__ = ( - "MongoEnvNotSet", - "OpenPypeMongoConnection", - "get_default_components", - "should_add_certificate_path_to_mongo_url", - "validate_mongo_connection", -) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index 0b6d0a3391..fec6a0c47d 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -2,59 +2,12 @@ import os import re import logging import platform -import functools -import warnings import clique log = logging.getLogger(__name__) -class PathToolsDeprecatedWarning(DeprecationWarning): - pass - - -def deprecated(new_destination): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - func = None - if callable(new_destination): - func = new_destination - new_destination = None - - def _decorator(decorated_func): - if new_destination is None: - warning_message = ( - " Please check content of deprecated function to figure out" - " possible replacement." - ) - else: - warning_message = " Please replace your usage with '{}'.".format( - new_destination - ) - - @functools.wraps(decorated_func) - def wrapper(*args, **kwargs): - warnings.simplefilter("always", PathToolsDeprecatedWarning) - warnings.warn( - ( - "Call to deprecated function '{}'" - "\nFunction was moved or removed.{}" - ).format(decorated_func.__name__, warning_message), - category=PathToolsDeprecatedWarning, - stacklevel=4 - ) - return decorated_func(*args, **kwargs) - return wrapper - - if func is None: - return _decorator - return _decorator(func) - - def format_file_size(file_size, suffix=None): """Returns formatted string with size in appropriate unit. @@ -269,99 +222,3 @@ def get_last_version_from_path(path_dir, filter): return filtred_files[-1] return None - - -@deprecated("openpype.pipeline.project_folders.concatenate_splitted_paths") -def concatenate_splitted_paths(split_paths, anatomy): - """ - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.project_folders import concatenate_splitted_paths - - return concatenate_splitted_paths(split_paths, anatomy) - - -@deprecated -def get_format_data(anatomy): - """ - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.template_data import get_project_template_data - - data = get_project_template_data(project_name=anatomy.project_name) - data["root"] = anatomy.roots - return data - - -@deprecated("openpype.pipeline.project_folders.fill_paths") -def fill_paths(path_list, anatomy): - """ - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.project_folders import fill_paths - - return fill_paths(path_list, anatomy) - - -@deprecated("openpype.pipeline.project_folders.create_project_folders") -def create_project_folders(basic_paths, project_name): - """ - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.project_folders import create_project_folders - - return create_project_folders(project_name, basic_paths) - - -@deprecated("openpype.pipeline.project_folders.get_project_basic_paths") -def get_project_basic_paths(project_name): - """ - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.project_folders import get_project_basic_paths - - return get_project_basic_paths(project_name) - - -@deprecated("openpype.pipeline.workfile.create_workdir_extra_folders") -def create_workdir_extra_folders( - workdir, host_name, task_type, task_name, project_name, - project_settings=None -): - """Create extra folders in work directory based on context. - - Args: - workdir (str): Path to workdir where workfiles is stored. - host_name (str): Name of host implementation. - task_type (str): Type of task for which extra folders should be - created. - task_name (str): Name of task for which extra folders should be - created. - project_name (str): Name of project on which task is. - project_settings (dict): Prepared project settings. Are loaded if not - passed. - - Deprecated: - Function will be removed after release version 3.16.* - """ - - from openpype.pipeline.project_folders import create_workdir_extra_folders - - return create_workdir_extra_folders( - workdir, - host_name, - task_type, - task_name, - project_name, - project_settings - ) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 10fd3940b8..d204fc2c8f 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -4,157 +4,9 @@ import os import logging import re -import warnings -import functools - -from openpype.client import get_asset_by_id - log = logging.getLogger(__name__) -class PluginToolsDeprecatedWarning(DeprecationWarning): - pass - - -def deprecated(new_destination): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - func = None - if callable(new_destination): - func = new_destination - new_destination = None - - def _decorator(decorated_func): - if new_destination is None: - warning_message = ( - " Please check content of deprecated function to figure out" - " possible replacement." - ) - else: - warning_message = " Please replace your usage with '{}'.".format( - new_destination - ) - - @functools.wraps(decorated_func) - def wrapper(*args, **kwargs): - warnings.simplefilter("always", PluginToolsDeprecatedWarning) - warnings.warn( - ( - "Call to deprecated function '{}'" - "\nFunction was moved or removed.{}" - ).format(decorated_func.__name__, warning_message), - category=PluginToolsDeprecatedWarning, - stacklevel=4 - ) - return decorated_func(*args, **kwargs) - return wrapper - - if func is None: - return _decorator - return _decorator(func) - - -@deprecated("openpype.pipeline.create.TaskNotSetError") -def TaskNotSetError(*args, **kwargs): - from openpype.pipeline.create import TaskNotSetError - - return TaskNotSetError(*args, **kwargs) - - -@deprecated("openpype.pipeline.create.get_subset_name") -def get_subset_name_with_asset_doc( - family, - variant, - task_name, - asset_doc, - project_name=None, - host_name=None, - default_template=None, - dynamic_data=None -): - """Calculate subset name based on passed context and OpenPype settings. - - Subst name templates are defined in `project_settings/global/tools/creator - /subset_name_profiles` where are profiles with host name, family, task name - and task type filters. If context does not match any profile then - `DEFAULT_SUBSET_TEMPLATE` is used as default template. - - That's main reason why so many arguments are required to calculate subset - name. - - Args: - family (str): Instance family. - variant (str): In most of cases it is user input during creation. - task_name (str): Task name on which context is instance created. - asset_doc (dict): Queried asset document with it's tasks in data. - Used to get task type. - project_name (str): Name of project on which is instance created. - Important for project settings that are loaded. - host_name (str): One of filtering criteria for template profile - filters. - default_template (str): Default template if any profile does not match - passed context. Constant 'DEFAULT_SUBSET_TEMPLATE' is used if - is not passed. - dynamic_data (dict): Dynamic data specific for a creator which creates - instance. - """ - - from openpype.pipeline.create import get_subset_name - - return get_subset_name( - family, - variant, - task_name, - asset_doc, - project_name, - host_name, - default_template, - dynamic_data - ) - - -@deprecated -def get_subset_name( - family, - variant, - task_name, - asset_id, - project_name=None, - host_name=None, - default_template=None, - dynamic_data=None, - dbcon=None -): - """Calculate subset name using OpenPype settings. - - This variant of function expects asset id as argument. - - This is legacy function should be replaced with - `get_subset_name_with_asset_doc` where asset document is expected. - """ - - from openpype.pipeline.create import get_subset_name - - if project_name is None: - project_name = dbcon.project_name - - asset_doc = get_asset_by_id(project_name, asset_id, fields=["data.tasks"]) - - return get_subset_name( - family, - variant, - task_name, - asset_doc, - project_name, - host_name, - default_template, - dynamic_data - ) - - def prepare_template_data(fill_pairs): """ Prepares formatted data for filling template. diff --git a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py index 43f5d1ef0e..db2e4eadc5 100644 --- a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py +++ b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py @@ -1,7 +1,5 @@ -import os - import pyblish.api -from openpype.lib.mongo import OpenPypeMongoConnection +from openpype.client.mongo import OpenPypeMongoConnection class CollectShotgridEntities(pyblish.api.ContextPlugin): diff --git a/openpype/plugin.py b/openpype/plugin.py deleted file mode 100644 index 7e906b4451..0000000000 --- a/openpype/plugin.py +++ /dev/null @@ -1,128 +0,0 @@ -import functools -import warnings - -import pyblish.api - -# New location of orders: openpype.pipeline.publish.constants -# - can be imported as -# 'from openpype.pipeline.publish import ValidatePipelineOrder' -ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05 -ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 -ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2 -ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 - - -class PluginDeprecatedWarning(DeprecationWarning): - pass - - -def _deprecation_warning(item_name, warning_message): - warnings.simplefilter("always", PluginDeprecatedWarning) - warnings.warn( - ( - "Call to deprecated function '{}'" - "\nFunction was moved or removed.{}" - ).format(item_name, warning_message), - category=PluginDeprecatedWarning, - stacklevel=4 - ) - - -def deprecated(new_destination): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - func = None - if callable(new_destination): - func = new_destination - new_destination = None - - def _decorator(decorated_func): - if new_destination is None: - warning_message = ( - " Please check content of deprecated function to figure out" - " possible replacement." - ) - else: - warning_message = " Please replace your usage with '{}'.".format( - new_destination - ) - - @functools.wraps(decorated_func) - def wrapper(*args, **kwargs): - _deprecation_warning(decorated_func.__name__, warning_message) - return decorated_func(*args, **kwargs) - return wrapper - - if func is None: - return _decorator - return _decorator(func) - - -# Classes just inheriting from pyblish classes -# - seems to be unused in code (not 100% sure) -# - they should be removed but because it is not clear if they're used -# we'll keep then and log deprecation warning -# Deprecated since 3.14.* will be removed in 3.16.* -class ContextPlugin(pyblish.api.ContextPlugin): - def __init__(self, *args, **kwargs): - _deprecation_warning( - "openpype.plugin.ContextPlugin", - " Please replace your usage with 'pyblish.api.ContextPlugin'." - ) - super(ContextPlugin, self).__init__(*args, **kwargs) - - -# Deprecated since 3.14.* will be removed in 3.16.* -class InstancePlugin(pyblish.api.InstancePlugin): - def __init__(self, *args, **kwargs): - _deprecation_warning( - "openpype.plugin.ContextPlugin", - " Please replace your usage with 'pyblish.api.InstancePlugin'." - ) - super(InstancePlugin, self).__init__(*args, **kwargs) - - -class Extractor(pyblish.api.InstancePlugin): - """Extractor base class. - - The extractor base class implements a "staging_dir" function used to - generate a temporary directory for an instance to extract to. - - This temporary directory is generated through `tempfile.mkdtemp()` - - """ - - order = 2.0 - - def staging_dir(self, instance): - """Provide a temporary directory in which to store extracted files - - Upon calling this method the staging directory is stored inside - the instance.data['stagingDir'] - """ - - from openpype.pipeline.publish import get_instance_staging_dir - - return get_instance_staging_dir(instance) - - -@deprecated("openpype.pipeline.publish.context_plugin_should_run") -def contextplugin_should_run(plugin, context): - """Return whether the ContextPlugin should run on the given context. - - This is a helper function to work around a bug pyblish-base#250 - Whenever a ContextPlugin sets specific families it will still trigger even - when no instances are present that have those families. - - This actually checks it correctly and returns whether it should run. - - Deprecated: - Since 3.14.* will be removed in 3.16.* or later. - """ - - from openpype.pipeline.publish import context_plugin_should_run - - return context_plugin_should_run(plugin, context) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 1d4c838f1a..671cabfbc2 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -1803,10 +1803,7 @@ class MongoLocalSettingsHandler(LocalSettingsHandler): def __init__(self, local_site_id=None): # Get mongo connection - from openpype.lib import ( - OpenPypeMongoConnection, - get_local_site_id - ) + from openpype.lib import get_local_site_id if local_site_id is None: local_site_id = get_local_site_id() From 476f018485f87617f168dd1fb9d63803b29d4100 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 25 Jul 2023 16:35:47 +0200 Subject: [PATCH 368/446] OP-4845 - use ordinary publish template resolving instead hardcoding Now it should use configuration in `tools/publish/template_name_profiles` instead of hardcoded value. --- .../plugins/publish/submit_publish_job.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index f912be1abe..fc119a655a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -16,9 +16,8 @@ from openpype.pipeline import ( legacy_io, ) from openpype.pipeline import publish -from openpype.lib import EnumDef +from openpype.lib import EnumDef, is_running_from_build from openpype.tests.lib import is_in_tests -from openpype.lib import is_running_from_build from openpype.pipeline.farm.pyblish_functions import ( create_skeleton_instance, @@ -185,7 +184,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, instance.data.get("asset"), instances[0]["subset"], instance.context, - 'render', + instances[0]["family"], override_version ) @@ -571,16 +570,21 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, else: version = 1 + host_name = context.data["hostName"] + task_info = template_data.get("task") or {} + + template_name = publish.get_publish_template_name( + project_name, + host_name, + family, + task_info.get("name"), + task_info.get("type"), + ) + template_data["subset"] = subset template_data["family"] = family template_data["version"] = version - # temporary fix, Ayon Settings don't have 'render' template, but they - # have "publish" TODO!!! - template_name = "render" - if os.environ.get("USE_AYON_SERVER") == '1': - template_name = "publish" - render_templates = anatomy.templates_obj[template_name] if "folder" in render_templates: publish_folder = render_templates["folder"].format_strict( From 8fe7ab25c5519d1d9c0f1acdc5e6696317c97898 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 25 Jul 2023 15:45:23 +0100 Subject: [PATCH 369/446] Added exception handling to UE Workers --- openpype/hosts/unreal/ue_workers.py | 39 ++++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index 3a0f976957..75487427d4 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -40,17 +40,34 @@ def retrieve_exit_code(line: str): return None -class UEProjectGenerationWorker(QtCore.QObject): +class UEWorker(QtCore.QObject): finished = QtCore.Signal(str) - failed = QtCore.Signal(str) + failed = QtCore.Signal(str, int) progress = QtCore.Signal(int) log = QtCore.Signal(str) + + engine_path: Path = None + env = None + + def execute(self): + raise NotImplementedError("Please implement this method!") + + def run(self): + try: + self.execute() + except Exception as e: + import traceback + self.log.emit(str(e)) + self.log.emit(traceback.format_exc()) + self.failed.emit(str(e), 1) + raise e + + +class UEProjectGenerationWorker(UEWorker): stage_begin = QtCore.Signal(str) ue_version: str = None project_name: str = None - env = None - engine_path: Path = None project_dir: Path = None dev_mode = False @@ -87,7 +104,7 @@ class UEProjectGenerationWorker(QtCore.QObject): self.project_name = unreal_project_name self.engine_path = engine_path - def run(self): + def execute(self): # engine_path should be the location of UE_X.X folder ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path, @@ -297,16 +314,8 @@ class UEProjectGenerationWorker(QtCore.QObject): self.progress.emit(100) self.finished.emit("Project successfully built!") - -class UEPluginInstallWorker(QtCore.QObject): - finished = QtCore.Signal(str) +class UEPluginInstallWorker(UEWorker): installing = QtCore.Signal(str) - failed = QtCore.Signal(str, int) - progress = QtCore.Signal(int) - log = QtCore.Signal(str) - - engine_path: Path = None - env = None def setup(self, engine_path: Path, env: dict = None, ): self.engine_path = engine_path @@ -374,7 +383,7 @@ class UEPluginInstallWorker(QtCore.QObject): dir_util.remove_tree(temp_dir.as_posix()) - def run(self): + def execute(self): src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", "")) if not os.path.isdir(src_plugin_dir): From 211e730673deb16d0d61ae9f8ceacdf7c8af4a6e Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 25 Jul 2023 23:22:21 +0300 Subject: [PATCH 370/446] add toggles --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 7 +++++-- openpype/hosts/houdini/plugins/create/create_karma_rop.py | 5 ++++- openpype/hosts/houdini/plugins/create/create_mantra_rop.py | 5 ++++- .../hosts/houdini/plugins/create/create_redshift_rop.py | 7 +++++-- openpype/hosts/houdini/plugins/create/create_vray_rop.py | 5 ++++- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index bddf26dbd5..ca516619f6 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -1,5 +1,5 @@ from openpype.hosts.houdini.api import plugin -from openpype.lib import EnumDef +from openpype.lib import EnumDef, BoolDef class CreateArnoldRop(plugin.HoudiniCreator): @@ -24,7 +24,7 @@ class CreateArnoldRop(plugin.HoudiniCreator): # Add chunk size attribute instance_data["chunkSize"] = 1 # Submit for job publishing - instance_data["farm"] = True + instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateArnoldRop, self).create( subset_name, @@ -64,6 +64,9 @@ class CreateArnoldRop(plugin.HoudiniCreator): ] return attrs + [ + BoolDef("farm", + label="Submitting to Farm", + default=True), EnumDef("image_format", image_format_enum, default=self.ext, diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index edfb992e1a..71c2bf1b28 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -21,7 +21,7 @@ class CreateKarmaROP(plugin.HoudiniCreator): # Add chunk size attribute instance_data["chunkSize"] = 10 # Submit for job publishing - instance_data["farm"] = True + instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateKarmaROP, self).create( subset_name, @@ -96,6 +96,9 @@ class CreateKarmaROP(plugin.HoudiniCreator): ] return attrs + [ + BoolDef("farm", + label="Submitting to Farm", + default=True), EnumDef("image_format", image_format_enum, default="exr", diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 5ca53e96de..5c29adb33f 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -21,7 +21,7 @@ class CreateMantraROP(plugin.HoudiniCreator): # Add chunk size attribute instance_data["chunkSize"] = 10 # Submit for job publishing - instance_data["farm"] = True + instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateMantraROP, self).create( subset_name, @@ -76,6 +76,9 @@ class CreateMantraROP(plugin.HoudiniCreator): ] return attrs + [ + BoolDef("farm", + label="Submitting to Farm", + default=True), EnumDef("image_format", image_format_enum, default="exr", diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 4576e9a721..8f4aa1327d 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -3,7 +3,7 @@ import hou # noqa from openpype.hosts.houdini.api import plugin -from openpype.lib import EnumDef +from openpype.lib import EnumDef, BoolDef class CreateRedshiftROP(plugin.HoudiniCreator): @@ -23,7 +23,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): # Add chunk size attribute instance_data["chunkSize"] = 10 # Submit for job publishing - instance_data["farm"] = True + instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateRedshiftROP, self).create( subset_name, @@ -100,6 +100,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator): ] return attrs + [ + BoolDef("farm", + label="Submitting to Farm", + default=True), EnumDef("image_format", image_format_enum, default=self.ext, diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 1de9be4ed6..58748d4c34 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -25,7 +25,7 @@ class CreateVrayROP(plugin.HoudiniCreator): # Add chunk size attribute instance_data["chunkSize"] = 10 # Submit for job publishing - instance_data["farm"] = True + instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateVrayROP, self).create( subset_name, @@ -139,6 +139,9 @@ class CreateVrayROP(plugin.HoudiniCreator): ] return attrs + [ + BoolDef("farm", + label="Submitting to Farm", + default=True), EnumDef("image_format", image_format_enum, default=self.ext, From bf600987d20410be43457b76269654fde5f9c31f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 26 Jul 2023 03:24:41 +0000 Subject: [PATCH 371/446] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 9a4fef421c..0a0b192892 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.2" +__version__ = "3.16.3-nightly.1" From 04ef421f41d16a433725b687cf946a9b60db1319 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Jul 2023 03:25:25 +0000 Subject: [PATCH 372/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e7717f395f..c71822db2d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.3-nightly.1 - 3.16.2 - 3.16.2-nightly.2 - 3.16.2-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.7-nightly.2 - 3.14.7-nightly.1 - 3.14.6 - - 3.14.6-nightly.3 validations: required: true - type: dropdown From debc9b6fd84df7c9ee092123616b48c90db0a125 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Jul 2023 12:19:38 +0100 Subject: [PATCH 373/446] Hound fixes --- openpype/hosts/unreal/ue_workers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index 75487427d4..386ad877d7 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -314,6 +314,7 @@ class UEProjectGenerationWorker(UEWorker): self.progress.emit(100) self.finished.emit("Project successfully built!") + class UEPluginInstallWorker(UEWorker): installing = QtCore.Signal(str) From e6d9697e23f38c1fa31e05ffdde0937ee335ac85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:40:29 +0200 Subject: [PATCH 374/446] Ftrack: Sync to avalon settings (#5353) * replace 'statuses_name_change' with 'role_list' in settings * use the settings in sync to avalon action --- .../event_handlers_server/action_sync_to_avalon.py | 10 ++++++++-- .../settings/defaults/project_settings/ftrack.json | 7 ++++--- .../schemas/projects_schema/schema_project_ftrack.json | 9 +++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/action_sync_to_avalon.py index df9147bdf7..442206feba 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -40,6 +40,7 @@ class SyncToAvalonServer(ServerAction): #: Action description. description = "Send data from Ftrack to Avalon" role_list = {"Pypeclub", "Administrator", "Project Manager"} + settings_key = "sync_to_avalon" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -48,11 +49,16 @@ class SyncToAvalonServer(ServerAction): def discover(self, session, entities, event): """ Validation """ # Check if selection is valid + is_valid = False for ent in event["data"]["selection"]: # Ignore entities that are not tasks or projects if ent["entityType"].lower() in ["show", "task"]: - return True - return False + is_valid = True + break + + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def launch(self, session, in_entities, event): self.log.debug("{}: Creating job".format(self.label)) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index b87c45666d..e2ca334b5f 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -1,9 +1,10 @@ { "events": { "sync_to_avalon": { - "statuses_name_change": [ - "ready", - "not ready" + "role_list": [ + "Pypeclub", + "Administrator", + "Project manager" ] }, "prepare_project": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 157a8d297e..d6efb118b9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -21,12 +21,9 @@ }, { "type": "list", - "key": "statuses_name_change", - "label": "Statuses", - "object_type": { - "type": "text", - "multiline": false - } + "key": "role_list", + "label": "Roles", + "object_type": "text" } ] }, From 2b37b8af48b58d61ce13b1228a6499b283bcf1bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Jul 2023 14:08:42 +0200 Subject: [PATCH 375/446] AYON: Addon settings in OpenPype (#5347) * copied addons from 'ayon-addon-settings' * added AE, photoshop and harmony addon * moved openpype to subfolder * cleanup repository files * updated create package script and README.md * formatting fixes * added cli flags to be able keep server structure * print progress and output dir * another formatting fixes --- .gitignore | 2 +- server_addon/README.md | 23 +- server_addon/aftereffects/LICENSE | 202 +++ server_addon/aftereffects/README.md | 4 + server_addon/aftereffects/server/__init__.py | 15 + .../aftereffects/server/settings/__init__.py | 10 + .../server/settings/creator_plugins.py | 16 + .../aftereffects/server/settings/imageio.py | 48 + .../aftereffects/server/settings/main.py | 62 + .../server/settings/publish_plugins.py | 36 + .../server/settings/workfile_builder.py | 25 + server_addon/aftereffects/server/version.py | 3 + server_addon/applications/server/__init__.py | 153 ++ .../applications/server/applications.json | 1125 +++++++++++++++ server_addon/applications/server/settings.py | 201 +++ server_addon/applications/server/tools.json | 55 + server_addon/applications/server/version.py | 1 + server_addon/blender/server/__init__.py | 19 + .../blender/server/settings/__init__.py | 10 + .../blender/server/settings/imageio.py | 48 + server_addon/blender/server/settings/main.py | 53 + .../server/settings/publish_plugins.py | 273 ++++ server_addon/blender/server/version.py | 1 + server_addon/celaction/server/__init__.py | 19 + server_addon/celaction/server/imageio.py | 48 + server_addon/celaction/server/settings.py | 92 ++ server_addon/celaction/server/version.py | 1 + server_addon/clockify/server/__init__.py | 15 + server_addon/clockify/server/settings.py | 9 + server_addon/clockify/server/version.py | 1 + server_addon/core/server/__init__.py | 14 + server_addon/core/server/settings/__init__.py | 7 + server_addon/core/server/settings/main.py | 160 +++ .../core/server/settings/publish_plugins.py | 959 +++++++++++++ server_addon/core/server/settings/tools.py | 506 +++++++ server_addon/core/server/version.py | 1 + server_addon/create_ayon_addon.py | 140 -- server_addon/create_ayon_addons.py | 279 ++++ server_addon/deadline/server/__init__.py | 17 + .../deadline/server/settings/__init__.py | 10 + server_addon/deadline/server/settings/main.py | 48 + .../server/settings/publish_plugins.py | 435 ++++++ server_addon/deadline/server/version.py | 1 + server_addon/flame/server/__init__.py | 19 + .../flame/server/settings/__init__.py | 10 + .../flame/server/settings/create_plugins.py | 120 ++ server_addon/flame/server/settings/imageio.py | 130 ++ .../flame/server/settings/loader_plugins.py | 99 ++ server_addon/flame/server/settings/main.py | 33 + .../flame/server/settings/publish_plugins.py | 190 +++ server_addon/flame/server/version.py | 1 + server_addon/fusion/server/__init__.py | 19 + server_addon/fusion/server/imageio.py | 48 + server_addon/fusion/server/settings.py | 95 ++ server_addon/fusion/server/version.py | 1 + server_addon/harmony/LICENSE | 202 +++ server_addon/harmony/README.md | 4 + server_addon/harmony/server/__init__.py | 15 + .../harmony/server/settings/__init__.py | 10 + .../harmony/server/settings/imageio.py | 55 + server_addon/harmony/server/settings/load.py | 20 + server_addon/harmony/server/settings/main.py | 68 + .../server/settings/publish_plugins.py | 76 + server_addon/harmony/server/version.py | 3 + server_addon/hiero/server/__init__.py | 19 + .../hiero/server/settings/__init__.py | 10 + server_addon/hiero/server/settings/common.py | 98 ++ .../hiero/server/settings/create_plugins.py | 97 ++ server_addon/hiero/server/settings/filters.py | 19 + server_addon/hiero/server/settings/imageio.py | 169 +++ .../hiero/server/settings/loader_plugins.py | 38 + server_addon/hiero/server/settings/main.py | 64 + .../hiero/server/settings/publish_plugins.py | 48 + .../hiero/server/settings/scriptsmenu.py | 41 + server_addon/hiero/server/version.py | 1 + server_addon/houdini/server/__init__.py | 17 + .../houdini/server/settings/__init__.py | 10 + .../houdini/server/settings/imageio.py | 48 + server_addon/houdini/server/settings/main.py | 79 ++ .../server/settings/publish_plugins.py | 150 ++ server_addon/houdini/server/version.py | 1 + server_addon/kitsu/server/__init__.py | 19 + server_addon/kitsu/server/settings.py | 111 ++ server_addon/kitsu/server/version.py | 1 + server_addon/maya/LICENCE | 201 +++ server_addon/maya/README.md | 4 + server_addon/maya/server/__init__.py | 16 + server_addon/maya/server/settings/__init__.py | 0 server_addon/maya/server/settings/creators.py | 408 ++++++ .../settings/explicit_plugins_loading.py | 429 ++++++ server_addon/maya/server/settings/imageio.py | 126 ++ .../maya/server/settings/include_handles.py | 30 + server_addon/maya/server/settings/loaders.py | 115 ++ server_addon/maya/server/settings/main.py | 139 ++ .../maya/server/settings/maya_dirmap.py | 40 + .../maya/server/settings/publish_playblast.py | 382 +++++ .../maya/server/settings/publishers.py | 1262 +++++++++++++++++ .../maya/server/settings/render_settings.py | 500 +++++++ .../maya/server/settings/scriptsmenu.py | 43 + .../settings/templated_workfile_settings.py | 25 + .../settings/workfile_build_settings.py | 131 ++ server_addon/maya/server/version.py | 3 + server_addon/muster/server/__init__.py | 17 + server_addon/muster/server/settings.py | 37 + server_addon/muster/server/version.py | 1 + server_addon/nuke/server/__init__.py | 17 + server_addon/nuke/server/settings/__init__.py | 10 + server_addon/nuke/server/settings/common.py | 128 ++ .../nuke/server/settings/create_plugins.py | 223 +++ server_addon/nuke/server/settings/dirmap.py | 47 + server_addon/nuke/server/settings/filters.py | 19 + server_addon/nuke/server/settings/general.py | 42 + server_addon/nuke/server/settings/gizmo.py | 79 ++ server_addon/nuke/server/settings/imageio.py | 410 ++++++ .../nuke/server/settings/loader_plugins.py | 80 ++ server_addon/nuke/server/settings/main.py | 128 ++ .../nuke/server/settings/publish_plugins.py | 536 +++++++ .../nuke/server/settings/scriptsmenu.py | 54 + .../settings/templated_workfile_build.py | 33 + .../nuke/server/settings/workfile_builder.py | 72 + server_addon/nuke/server/version.py | 1 + .../{ => openpype}/client/pyproject.toml | 0 .../{ => openpype}/server/__init__.py | 0 server_addon/photoshop/LICENSE | 202 +++ server_addon/photoshop/README.md | 4 + server_addon/photoshop/server/__init__.py | 15 + .../photoshop/server/settings/__init__.py | 10 + .../server/settings/creator_plugins.py | 79 ++ .../photoshop/server/settings/imageio.py | 64 + .../photoshop/server/settings/main.py | 41 + .../server/settings/publish_plugins.py | 221 +++ .../server/settings/workfile_builder.py | 41 + server_addon/photoshop/server/version.py | 3 + server_addon/resolve/server/__init__.py | 19 + server_addon/resolve/server/imageio.py | 64 + server_addon/resolve/server/settings.py | 114 ++ server_addon/resolve/server/version.py | 1 + server_addon/royal_render/server/__init__.py | 17 + server_addon/royal_render/server/settings.py | 53 + server_addon/royal_render/server/version.py | 1 + .../timers_manager/server/__init__.py | 13 + .../timers_manager/server/settings.py | 9 + server_addon/timers_manager/server/version.py | 1 + server_addon/traypublisher/server/LICENSE | 202 +++ server_addon/traypublisher/server/README.md | 4 + server_addon/traypublisher/server/__init__.py | 15 + .../traypublisher/server/settings/__init__.py | 10 + .../server/settings/creator_plugins.py | 46 + .../server/settings/editorial_creators.py | 181 +++ .../traypublisher/server/settings/imageio.py | 48 + .../traypublisher/server/settings/main.py | 52 + .../server/settings/publish_plugins.py | 41 + .../server/settings/simple_creators.py | 292 ++++ server_addon/traypublisher/server/version.py | 3 + server_addon/tvpaint/server/__init__.py | 17 + .../tvpaint/server/settings/__init__.py | 10 + .../tvpaint/server/settings/create_plugins.py | 133 ++ .../tvpaint/server/settings/filters.py | 19 + .../tvpaint/server/settings/imageio.py | 48 + server_addon/tvpaint/server/settings/main.py | 90 ++ .../server/settings/publish_plugins.py | 132 ++ .../server/settings/workfile_builder.py | 30 + server_addon/tvpaint/server/version.py | 1 + server_addon/unreal/server/__init__.py | 19 + server_addon/unreal/server/imageio.py | 48 + server_addon/unreal/server/settings.py | 64 + server_addon/unreal/server/version.py | 1 + 167 files changed, 15525 insertions(+), 146 deletions(-) create mode 100644 server_addon/aftereffects/LICENSE create mode 100644 server_addon/aftereffects/README.md create mode 100644 server_addon/aftereffects/server/__init__.py create mode 100644 server_addon/aftereffects/server/settings/__init__.py create mode 100644 server_addon/aftereffects/server/settings/creator_plugins.py create mode 100644 server_addon/aftereffects/server/settings/imageio.py create mode 100644 server_addon/aftereffects/server/settings/main.py create mode 100644 server_addon/aftereffects/server/settings/publish_plugins.py create mode 100644 server_addon/aftereffects/server/settings/workfile_builder.py create mode 100644 server_addon/aftereffects/server/version.py create mode 100644 server_addon/applications/server/__init__.py create mode 100644 server_addon/applications/server/applications.json create mode 100644 server_addon/applications/server/settings.py create mode 100644 server_addon/applications/server/tools.json create mode 100644 server_addon/applications/server/version.py create mode 100644 server_addon/blender/server/__init__.py create mode 100644 server_addon/blender/server/settings/__init__.py create mode 100644 server_addon/blender/server/settings/imageio.py create mode 100644 server_addon/blender/server/settings/main.py create mode 100644 server_addon/blender/server/settings/publish_plugins.py create mode 100644 server_addon/blender/server/version.py create mode 100644 server_addon/celaction/server/__init__.py create mode 100644 server_addon/celaction/server/imageio.py create mode 100644 server_addon/celaction/server/settings.py create mode 100644 server_addon/celaction/server/version.py create mode 100644 server_addon/clockify/server/__init__.py create mode 100644 server_addon/clockify/server/settings.py create mode 100644 server_addon/clockify/server/version.py create mode 100644 server_addon/core/server/__init__.py create mode 100644 server_addon/core/server/settings/__init__.py create mode 100644 server_addon/core/server/settings/main.py create mode 100644 server_addon/core/server/settings/publish_plugins.py create mode 100644 server_addon/core/server/settings/tools.py create mode 100644 server_addon/core/server/version.py delete mode 100644 server_addon/create_ayon_addon.py create mode 100644 server_addon/create_ayon_addons.py create mode 100644 server_addon/deadline/server/__init__.py create mode 100644 server_addon/deadline/server/settings/__init__.py create mode 100644 server_addon/deadline/server/settings/main.py create mode 100644 server_addon/deadline/server/settings/publish_plugins.py create mode 100644 server_addon/deadline/server/version.py create mode 100644 server_addon/flame/server/__init__.py create mode 100644 server_addon/flame/server/settings/__init__.py create mode 100644 server_addon/flame/server/settings/create_plugins.py create mode 100644 server_addon/flame/server/settings/imageio.py create mode 100644 server_addon/flame/server/settings/loader_plugins.py create mode 100644 server_addon/flame/server/settings/main.py create mode 100644 server_addon/flame/server/settings/publish_plugins.py create mode 100644 server_addon/flame/server/version.py create mode 100644 server_addon/fusion/server/__init__.py create mode 100644 server_addon/fusion/server/imageio.py create mode 100644 server_addon/fusion/server/settings.py create mode 100644 server_addon/fusion/server/version.py create mode 100644 server_addon/harmony/LICENSE create mode 100644 server_addon/harmony/README.md create mode 100644 server_addon/harmony/server/__init__.py create mode 100644 server_addon/harmony/server/settings/__init__.py create mode 100644 server_addon/harmony/server/settings/imageio.py create mode 100644 server_addon/harmony/server/settings/load.py create mode 100644 server_addon/harmony/server/settings/main.py create mode 100644 server_addon/harmony/server/settings/publish_plugins.py create mode 100644 server_addon/harmony/server/version.py create mode 100644 server_addon/hiero/server/__init__.py create mode 100644 server_addon/hiero/server/settings/__init__.py create mode 100644 server_addon/hiero/server/settings/common.py create mode 100644 server_addon/hiero/server/settings/create_plugins.py create mode 100644 server_addon/hiero/server/settings/filters.py create mode 100644 server_addon/hiero/server/settings/imageio.py create mode 100644 server_addon/hiero/server/settings/loader_plugins.py create mode 100644 server_addon/hiero/server/settings/main.py create mode 100644 server_addon/hiero/server/settings/publish_plugins.py create mode 100644 server_addon/hiero/server/settings/scriptsmenu.py create mode 100644 server_addon/hiero/server/version.py create mode 100644 server_addon/houdini/server/__init__.py create mode 100644 server_addon/houdini/server/settings/__init__.py create mode 100644 server_addon/houdini/server/settings/imageio.py create mode 100644 server_addon/houdini/server/settings/main.py create mode 100644 server_addon/houdini/server/settings/publish_plugins.py create mode 100644 server_addon/houdini/server/version.py create mode 100644 server_addon/kitsu/server/__init__.py create mode 100644 server_addon/kitsu/server/settings.py create mode 100644 server_addon/kitsu/server/version.py create mode 100644 server_addon/maya/LICENCE create mode 100644 server_addon/maya/README.md create mode 100644 server_addon/maya/server/__init__.py create mode 100644 server_addon/maya/server/settings/__init__.py create mode 100644 server_addon/maya/server/settings/creators.py create mode 100644 server_addon/maya/server/settings/explicit_plugins_loading.py create mode 100644 server_addon/maya/server/settings/imageio.py create mode 100644 server_addon/maya/server/settings/include_handles.py create mode 100644 server_addon/maya/server/settings/loaders.py create mode 100644 server_addon/maya/server/settings/main.py create mode 100644 server_addon/maya/server/settings/maya_dirmap.py create mode 100644 server_addon/maya/server/settings/publish_playblast.py create mode 100644 server_addon/maya/server/settings/publishers.py create mode 100644 server_addon/maya/server/settings/render_settings.py create mode 100644 server_addon/maya/server/settings/scriptsmenu.py create mode 100644 server_addon/maya/server/settings/templated_workfile_settings.py create mode 100644 server_addon/maya/server/settings/workfile_build_settings.py create mode 100644 server_addon/maya/server/version.py create mode 100644 server_addon/muster/server/__init__.py create mode 100644 server_addon/muster/server/settings.py create mode 100644 server_addon/muster/server/version.py create mode 100644 server_addon/nuke/server/__init__.py create mode 100644 server_addon/nuke/server/settings/__init__.py create mode 100644 server_addon/nuke/server/settings/common.py create mode 100644 server_addon/nuke/server/settings/create_plugins.py create mode 100644 server_addon/nuke/server/settings/dirmap.py create mode 100644 server_addon/nuke/server/settings/filters.py create mode 100644 server_addon/nuke/server/settings/general.py create mode 100644 server_addon/nuke/server/settings/gizmo.py create mode 100644 server_addon/nuke/server/settings/imageio.py create mode 100644 server_addon/nuke/server/settings/loader_plugins.py create mode 100644 server_addon/nuke/server/settings/main.py create mode 100644 server_addon/nuke/server/settings/publish_plugins.py create mode 100644 server_addon/nuke/server/settings/scriptsmenu.py create mode 100644 server_addon/nuke/server/settings/templated_workfile_build.py create mode 100644 server_addon/nuke/server/settings/workfile_builder.py create mode 100644 server_addon/nuke/server/version.py rename server_addon/{ => openpype}/client/pyproject.toml (100%) rename server_addon/{ => openpype}/server/__init__.py (100%) create mode 100644 server_addon/photoshop/LICENSE create mode 100644 server_addon/photoshop/README.md create mode 100644 server_addon/photoshop/server/__init__.py create mode 100644 server_addon/photoshop/server/settings/__init__.py create mode 100644 server_addon/photoshop/server/settings/creator_plugins.py create mode 100644 server_addon/photoshop/server/settings/imageio.py create mode 100644 server_addon/photoshop/server/settings/main.py create mode 100644 server_addon/photoshop/server/settings/publish_plugins.py create mode 100644 server_addon/photoshop/server/settings/workfile_builder.py create mode 100644 server_addon/photoshop/server/version.py create mode 100644 server_addon/resolve/server/__init__.py create mode 100644 server_addon/resolve/server/imageio.py create mode 100644 server_addon/resolve/server/settings.py create mode 100644 server_addon/resolve/server/version.py create mode 100644 server_addon/royal_render/server/__init__.py create mode 100644 server_addon/royal_render/server/settings.py create mode 100644 server_addon/royal_render/server/version.py create mode 100644 server_addon/timers_manager/server/__init__.py create mode 100644 server_addon/timers_manager/server/settings.py create mode 100644 server_addon/timers_manager/server/version.py create mode 100644 server_addon/traypublisher/server/LICENSE create mode 100644 server_addon/traypublisher/server/README.md create mode 100644 server_addon/traypublisher/server/__init__.py create mode 100644 server_addon/traypublisher/server/settings/__init__.py create mode 100644 server_addon/traypublisher/server/settings/creator_plugins.py create mode 100644 server_addon/traypublisher/server/settings/editorial_creators.py create mode 100644 server_addon/traypublisher/server/settings/imageio.py create mode 100644 server_addon/traypublisher/server/settings/main.py create mode 100644 server_addon/traypublisher/server/settings/publish_plugins.py create mode 100644 server_addon/traypublisher/server/settings/simple_creators.py create mode 100644 server_addon/traypublisher/server/version.py create mode 100644 server_addon/tvpaint/server/__init__.py create mode 100644 server_addon/tvpaint/server/settings/__init__.py create mode 100644 server_addon/tvpaint/server/settings/create_plugins.py create mode 100644 server_addon/tvpaint/server/settings/filters.py create mode 100644 server_addon/tvpaint/server/settings/imageio.py create mode 100644 server_addon/tvpaint/server/settings/main.py create mode 100644 server_addon/tvpaint/server/settings/publish_plugins.py create mode 100644 server_addon/tvpaint/server/settings/workfile_builder.py create mode 100644 server_addon/tvpaint/server/version.py create mode 100644 server_addon/unreal/server/__init__.py create mode 100644 server_addon/unreal/server/imageio.py create mode 100644 server_addon/unreal/server/settings.py create mode 100644 server_addon/unreal/server/version.py diff --git a/.gitignore b/.gitignore index e5019a4e74..622d55fb88 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,7 @@ Temporary Items ########### /build /dist/ -/server_addon/package/* +/server_addon/packages/* /vendor/bin/* /vendor/python/* diff --git a/server_addon/README.md b/server_addon/README.md index fa9a6001d2..c6d467adaa 100644 --- a/server_addon/README.md +++ b/server_addon/README.md @@ -1,5 +1,5 @@ -# OpenPype addon for AYON server -Convert openpype into AYON addon which can be installed on AYON server. The versioning of the addon is following versioning of OpenPype. +# Addons for AYON server +Preparation of AYON addons based on OpenPype codebase. The output is a bunch of zip files in `./packages` directory that can be uploaded to AYON server. One of the packages is `openpype` which is OpenPype code converted to AYON addon. The addon is must have requirement to be able to use `ayon-launcher`. The versioning of `openpype` addon is following versioning of OpenPype. The other addons contain only settings models. ## Intro OpenPype is transitioning to AYON, a dedicated server with its own database, moving away from MongoDB. During this transition period, OpenPype will remain compatible with both MongoDB and AYON. However, we will gradually update the codebase to align with AYON's data structure and separate individual components into addons. @@ -11,11 +11,24 @@ Since the implementation of the AYON Launcher is not yet fully completed, we wil During this transitional period, the AYON Launcher addon will be a requirement as the entry point for using the AYON Launcher. ## How to start -There is a `create_ayon_addon.py` python file which contains logic how to create server addon from OpenPype codebase. Just run the code. +There is a `create_ayon_addons.py` python file which contains logic how to create server addon from OpenPype codebase. Just run the code. ```shell -./.poetry/bin/poetry run python ./server_addon/create_ayon_addon.py +./.poetry/bin/poetry run python ./server_addon/create_ayon_addons.py ``` -It will create directory `./package/openpype//*` folder with all files necessary for AYON server. You can then copy `./package/openpype/` to server addons, or zip the folder and upload it to AYON server. Restart server to update addons information, add the addon version to server bundle and set the bundle for production or staging usage. +It will create directory `./packages/.zip` files for AYON server. You can then copy upload the zip files to AYON server. Restart server to update addons information, add the addon version to server bundle and set the bundle for production or staging usage. Once addon is on server and is enabled, you can just run AYON launcher. Content will be downloaded and used automatically. + +### Additional arguments +Additional arguments are useful for development purposes. + +To skip zip creation to keep only server ready folder structure, pass `--skip-zip` argument. +```shell +./.poetry/bin/poetry run python ./server_addon/create_ayon_addons.py --skip-zip +``` + +To create both zips and keep folder structure, pass `--keep-sources` argument. +```shell +./.poetry/bin/poetry run python ./server_addon/create_ayon_addons.py --keep-sources +``` diff --git a/server_addon/aftereffects/LICENSE b/server_addon/aftereffects/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/server_addon/aftereffects/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/server_addon/aftereffects/README.md b/server_addon/aftereffects/README.md new file mode 100644 index 0000000000..b2f34f3407 --- /dev/null +++ b/server_addon/aftereffects/README.md @@ -0,0 +1,4 @@ +AfterEffects Addon +=============== + +Integration with Adobe AfterEffects. diff --git a/server_addon/aftereffects/server/__init__.py b/server_addon/aftereffects/server/__init__.py new file mode 100644 index 0000000000..e895c07ce1 --- /dev/null +++ b/server_addon/aftereffects/server/__init__.py @@ -0,0 +1,15 @@ +from ayon_server.addons import BaseServerAddon + +from .settings import AfterEffectsSettings, DEFAULT_AFTEREFFECTS_SETTING +from .version import __version__ + + +class AfterEffects(BaseServerAddon): + name = "aftereffects" + version = __version__ + + settings_model = AfterEffectsSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_AFTEREFFECTS_SETTING) diff --git a/server_addon/aftereffects/server/settings/__init__.py b/server_addon/aftereffects/server/settings/__init__.py new file mode 100644 index 0000000000..4e96804b4a --- /dev/null +++ b/server_addon/aftereffects/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + AfterEffectsSettings, + DEFAULT_AFTEREFFECTS_SETTING, +) + + +__all__ = ( + "AfterEffectsSettings", + "DEFAULT_AFTEREFFECTS_SETTING", +) diff --git a/server_addon/aftereffects/server/settings/creator_plugins.py b/server_addon/aftereffects/server/settings/creator_plugins.py new file mode 100644 index 0000000000..fee01bad26 --- /dev/null +++ b/server_addon/aftereffects/server/settings/creator_plugins.py @@ -0,0 +1,16 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class CreateRenderPlugin(BaseSettingsModel): + mark_for_review: bool = Field(True, title="Review") + defaults: list[str] = Field(default_factory=list, + title="Default Variants") + + +class AfterEffectsCreatorPlugins(BaseSettingsModel): + RenderCreator: CreateRenderPlugin = Field( + title="Create Render", + default_factory=CreateRenderPlugin, + ) diff --git a/server_addon/aftereffects/server/settings/imageio.py b/server_addon/aftereffects/server/settings/imageio.py new file mode 100644 index 0000000000..55160ffd11 --- /dev/null +++ b/server_addon/aftereffects/server/settings/imageio.py @@ -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 AfterEffectsImageIOModel(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" + ) diff --git a/server_addon/aftereffects/server/settings/main.py b/server_addon/aftereffects/server/settings/main.py new file mode 100644 index 0000000000..9da872bd92 --- /dev/null +++ b/server_addon/aftereffects/server/settings/main.py @@ -0,0 +1,62 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + +from .imageio import AfterEffectsImageIOModel +from .creator_plugins import AfterEffectsCreatorPlugins +from .publish_plugins import AfterEffectsPublishPlugins +from .workfile_builder import WorkfileBuilderPlugin + + +class AfterEffectsSettings(BaseSettingsModel): + """AfterEffects Project Settings.""" + + imageio: AfterEffectsImageIOModel = Field( + default_factory=AfterEffectsImageIOModel, + title="OCIO config" + ) + create: AfterEffectsCreatorPlugins = Field( + default_factory=AfterEffectsCreatorPlugins, + title="Creator plugins" + ) + + publish: AfterEffectsPublishPlugins = Field( + default_factory=AfterEffectsPublishPlugins, + title="Publish plugins" + ) + + workfile_builder: WorkfileBuilderPlugin = Field( + default_factory=WorkfileBuilderPlugin, + title="Workfile Builder" + ) + + +DEFAULT_AFTEREFFECTS_SETTING = { + "create": { + "RenderCreator": { + "mark_for_review": True, + "defaults": [ + "Main" + ] + } + }, + "publish": { + "CollectReview": { + "enabled": True + }, + "ValidateSceneSettings": { + "enabled": True, + "optional": True, + "active": True, + "skip_resolution_check": [ + ".*" + ], + "skip_timelines_check": [ + ".*" + ] + } + }, + "workfile_builder": { + "create_first_version": False, + "custom_templates": [] + } +} diff --git a/server_addon/aftereffects/server/settings/publish_plugins.py b/server_addon/aftereffects/server/settings/publish_plugins.py new file mode 100644 index 0000000000..0d90b08b5a --- /dev/null +++ b/server_addon/aftereffects/server/settings/publish_plugins.py @@ -0,0 +1,36 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class CollectReviewPluginModel(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + + +class ValidateSceneSettingsPlugin(BaseSettingsModel): + """Validate naming of products and layers""" # + _isGroup = True + enabled: bool = True + optional: bool = Field(False, title="Optional") + active: bool = Field(True, title="Active") + + skip_resolution_check: list[str] = Field( + default_factory=list, + title="Skip Resolution Check for Tasks" + ) + + skip_timelines_check: list[str] = Field( + default_factory=list, + title="Skip Timeline Check for Tasks" + ) + + +class AfterEffectsPublishPlugins(BaseSettingsModel): + CollectReview: CollectReviewPluginModel = Field( + default_facotory=CollectReviewPluginModel, + title="Collect Review" + ) + ValidateSceneSettings: ValidateSceneSettingsPlugin = Field( + title="Validate Scene Settings", + default_factory=ValidateSceneSettingsPlugin, + ) diff --git a/server_addon/aftereffects/server/settings/workfile_builder.py b/server_addon/aftereffects/server/settings/workfile_builder.py new file mode 100644 index 0000000000..d9d5fa41bf --- /dev/null +++ b/server_addon/aftereffects/server/settings/workfile_builder.py @@ -0,0 +1,25 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel, MultiplatformPathModel + + +class CustomBuilderTemplate(BaseSettingsModel): + task_types: list[str] = Field( + default_factory=list, + title="Task types", + ) + template_path: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel + ) + + +class WorkfileBuilderPlugin(BaseSettingsModel): + _title = "Workfile Builder" + create_first_version: bool = Field( + False, + title="Create first workfile" + ) + + custom_templates: list[CustomBuilderTemplate] = Field( + default_factory=CustomBuilderTemplate + ) diff --git a/server_addon/aftereffects/server/version.py b/server_addon/aftereffects/server/version.py new file mode 100644 index 0000000000..d4b9e2d7f3 --- /dev/null +++ b/server_addon/aftereffects/server/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Package declaring addon version.""" +__version__ = "0.1.0" diff --git a/server_addon/applications/server/__init__.py b/server_addon/applications/server/__init__.py new file mode 100644 index 0000000000..a3fd92eb6e --- /dev/null +++ b/server_addon/applications/server/__init__.py @@ -0,0 +1,153 @@ +import os +import json +import copy + +from ayon_server.addons import BaseServerAddon +from ayon_server.lib.postgres import Postgres + +from .version import __version__ +from .settings import ApplicationsAddonSettings, DEFAULT_VALUES + + +def get_enum_items_from_groups(groups): + label_by_name = {} + for group in groups: + group_name = group["name"] + group_label = group["label"] or group_name + for variant in group["variants"]: + variant_name = variant["name"] + if not variant_name: + continue + variant_label = variant["label"] or variant_name + full_name = f"{group_name}/{variant_name}" + full_label = f"{group_label} {variant_label}" + label_by_name[full_name] = full_label + enum_items = [] + for full_name in sorted(label_by_name): + enum_items.append( + {"value": full_name, "label": label_by_name[full_name]} + ) + return enum_items + + +class ApplicationsAddon(BaseServerAddon): + name = "applications" + version = __version__ + settings_model = ApplicationsAddonSettings + + async def get_default_settings(self): + applications_path = os.path.join(self.addon_dir, "applications.json") + tools_path = os.path.join(self.addon_dir, "tools.json") + default_values = copy.deepcopy(DEFAULT_VALUES) + with open(applications_path, "r") as stream: + default_values.update(json.load(stream)) + + with open(tools_path, "r") as stream: + default_values.update(json.load(stream)) + + return self.get_settings_model()(**default_values) + + async def setup(self): + need_restart = await self.create_applications_attribute() + if need_restart: + self.request_server_restart() + + async def create_applications_attribute(self) -> bool: + """Make sure there are required attributes which ftrack addon needs. + + Returns: + bool: 'True' if an attribute was created or updated. + """ + + settings_model = await self.get_studio_settings() + studio_settings = settings_model.dict() + applications = studio_settings["applications"] + _applications = applications.pop("additional_apps") + for name, value in applications.items(): + value["name"] = name + _applications.append(value) + + query = "SELECT name, position, scope, data from public.attributes" + + apps_attrib_name = "applications" + tools_attrib_name = "tools" + + apps_enum = get_enum_items_from_groups(_applications) + tools_enum = get_enum_items_from_groups(studio_settings["tool_groups"]) + apps_attribute_data = { + "type": "list_of_strings", + "title": "Applications", + "enum": apps_enum + } + tools_attribute_data = { + "type": "list_of_strings", + "title": "Tools", + "enum": tools_enum + } + apps_scope = ["project"] + tools_scope = ["project", "folder", "task"] + + apps_match_position = None + apps_matches = False + tools_match_position = None + tools_matches = False + position = 1 + async for row in Postgres.iterate(query): + position += 1 + if row["name"] == apps_attrib_name: + # Check if scope is matching ftrack addon requirements + if ( + set(row["scope"]) == set(apps_scope) + and row["data"].get("enum") == apps_enum + ): + apps_matches = True + apps_match_position = row["position"] + + elif row["name"] == tools_attrib_name: + if ( + set(row["scope"]) == set(tools_scope) + and row["data"].get("enum") == tools_enum + ): + tools_matches = True + tools_match_position = row["position"] + + if apps_matches and tools_matches: + return False + + postgre_query = "\n".join(( + "INSERT INTO public.attributes", + " (name, position, scope, data)", + "VALUES", + " ($1, $2, $3, $4)", + "ON CONFLICT (name)", + "DO UPDATE SET", + " scope = $3,", + " data = $4", + )) + if not apps_matches: + # Reuse position from found attribute + if apps_match_position is None: + apps_match_position = position + position += 1 + + await Postgres.execute( + postgre_query, + apps_attrib_name, + apps_match_position, + apps_scope, + apps_attribute_data, + ) + + if not tools_matches: + if tools_match_position is None: + tools_match_position = position + position += 1 + + await Postgres.execute( + postgre_query, + tools_attrib_name, + tools_match_position, + tools_scope, + tools_attribute_data, + ) + return True diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json new file mode 100644 index 0000000000..b19308ee7c --- /dev/null +++ b/server_addon/applications/server/applications.json @@ -0,0 +1,1125 @@ +{ + "applications": { + "maya": { + "enabled": true, + "label": "Maya", + "icon": "{}/app_icons/maya.png", + "host_name": "maya", + "environment": "{\n \"MAYA_DISABLE_CLIC_IPM\": \"Yes\",\n \"MAYA_DISABLE_CIP\": \"Yes\",\n \"MAYA_DISABLE_CER\": \"Yes\",\n \"PYMEL_SKIP_MEL_INIT\": \"Yes\",\n \"LC_ALL\": \"C\"\n}\n", + "variants": [ + { + "name": "2023", + "label": "2023", + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2023\\bin\\maya.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2023/bin/maya" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"MAYA_VERSION\": \"2023\"\n}", + "use_python_2": false + }, + { + "name": "2022", + "label": "2022", + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2022\\bin\\maya.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2022/bin/maya" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"MAYA_VERSION\": \"2022\"\n}", + "use_python_2": false + }, + { + "name": "2020", + "label": "2020", + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2020/bin/maya" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"MAYA_VERSION\": \"2020\"\n}", + "use_python_2": true + }, + { + "name": "2019", + "label": "2019", + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2019/bin/maya" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"MAYA_VERSION\": \"2019\"\n}", + "use_python_2": true + }, + { + "name": "2018", + "label": "2018", + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2018\\bin\\maya.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2018/bin/maya" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"MAYA_VERSION\": \"2018\"\n}", + "use_python_2": true + } + ] + }, + "adsk_3dsmax": { + "enabled": true, + "label": "3ds Max", + "icon": "{}/app_icons/3dsmax.png", + "host_name": "max", + "environment": "{\n \"ADSK_3DSMAX_STARTUPSCRIPTS_ADDON_DIR\": \"{OPENPYPE_ROOT}/openpype/hosts/max/startup\"\n}", + "variants": [ + { + "name": "2023", + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\3ds Max 2023\\3dsmax.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [ + "-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms" + ], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"3DSMAX_VERSION\": \"2023\"\n}" + } + ] + }, + "flame": { + "enabled": true, + "label": "Flame", + "icon": "{}/app_icons/flame.png", + "host_name": "flame", + "environment": "{\n \"FLAME_SCRIPT_DIRS\": {\n \"windows\": \"\",\n \"darwin\": \"\",\n \"linux\": \"\"\n },\n \"FLAME_WIRETAP_HOSTNAME\": \"\",\n \"FLAME_WIRETAP_VOLUME\": \"stonefs\",\n \"FLAME_WIRETAP_GROUP\": \"staff\"\n}", + "variants": [ + { + "name": "2021", + "label": "2021", + "executables": { + "windows": [], + "darwin": [ + "/opt/Autodesk/flame_2021/bin/flame.app/Contents/MacOS/startApp" + ], + "linux": [ + "/opt/Autodesk/flame_2021/bin/startApplication" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"OPENPYPE_FLAME_PYTHON_EXEC\": \"/opt/Autodesk/python/2021/bin/python2.7\",\n \"OPENPYPE_FLAME_PYTHONPATH\": \"/opt/Autodesk/flame_2021/python\",\n \"OPENPYPE_WIRETAP_TOOLS\": \"/opt/Autodesk/wiretap/tools/2021\"\n}", + "use_python_2": true + }, + { + "name": "2021_1", + "label": "2021.1", + "executables": { + "windows": [], + "darwin": [ + "/opt/Autodesk/flame_2021.1/bin/flame.app/Contents/MacOS/startApp" + ], + "linux": [ + "/opt/Autodesk/flame_2021.1/bin/startApplication" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"OPENPYPE_FLAME_PYTHON_EXEC\": \"/opt/Autodesk/python/2021.1/bin/python2.7\",\n \"OPENPYPE_FLAME_PYTHONPATH\": \"/opt/Autodesk/flame_2021.1/python\",\n \"OPENPYPE_WIRETAP_TOOLS\": \"/opt/Autodesk/wiretap/tools/2021.1\"\n}", + "use_python_2": true + } + ] + }, + "nuke": { + "enabled": true, + "label": "Nuke", + "icon": "{}/app_icons/nuke.png", + "host_name": "nuke", + "environment": "{\n \"NUKE_PATH\": [\n \"{NUKE_PATH}\",\n \"{OPENPYPE_STUDIO_PLUGINS}/nuke\"\n ]\n}", + "variants": [ + { + "name": "14-0", + "label": "14.0", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke14.0v4\\Nuke14.0.exe" + ], + "darwin": [ + "/Applications/Nuke14.0v4/Nuke14.0v4.app" + ], + "linux": [ + "/usr/local/Nuke14.0v4/Nuke14.0" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-2", + "label": "13.2", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v5\\Nuke13.2.exe" + ], + "darwin": [ + "/Applications/Nuke13.2v5/Nuke13.2v5.app" + ], + "linux": [ + "/usr/local/Nuke13.2v5/Nuke13.2" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-0", + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe" + ], + "darwin": [ + "/Applications/Nuke13.0v1/Nuke13.0v1.app" + ], + "linux": [ + "/usr/local/Nuke13.0v1/Nuke13.0" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, + "nukeassist": { + "enabled": true, + "label": "Nuke Assist", + "icon": "{}/app_icons/nuke.png", + "host_name": "nuke", + "environment": "{\n \"NUKE_PATH\": [\n \"{NUKE_PATH}\",\n \"{OPENPYPE_STUDIO_PLUGINS}/nuke\"\n ]\n}", + "variants": [ + { + "name": "14-0", + "label": "14.0", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke14.0v4\\Nuke14.0.exe" + ], + "darwin": [ + "/Applications/Nuke14.0v4/NukeAssist14.0v4.app" + ], + "linux": [ + "/usr/local/Nuke14.0v4/Nuke14.0" + ] + }, + "arguments": { + "windows": [ + "--nukeassist" + ], + "darwin": [], + "linux": [ + "--nukeassist" + ] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-2", + "label": "13.2", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v5\\Nuke13.2.exe" + ], + "darwin": [ + "/Applications/Nuke13.2v5/NukeAssist13.2v5.app" + ], + "linux": [ + "/usr/local/Nuke13.2v5/Nuke13.2" + ] + }, + "arguments": { + "windows": [ + "--nukeassist" + ], + "darwin": [], + "linux": [ + "--nukeassist" + ] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-0", + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe" + ], + "darwin": [ + "/Applications/Nuke13.0v1/NukeAssist13.0v1.app" + ], + "linux": [ + "/usr/local/Nuke13.0v1/Nuke13.0" + ] + }, + "arguments": { + "windows": [ + "--nukeassist" + ], + "darwin": [], + "linux": [ + "--nukeassist" + ] + }, + "environment": "{}" + } + ] + }, + "nukex": { + "enabled": true, + "label": "Nuke X", + "icon": "{}/app_icons/nukex.png", + "host_name": "nuke", + "environment": "{\n \"NUKE_PATH\": [\n \"{NUKE_PATH}\",\n \"{OPENPYPE_STUDIO_PLUGINS}/nuke\"\n ]\n}", + "variants": [ + { + "name": "14-0", + "label": "14.0", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke14.0v4\\Nuke14.0.exe" + ], + "darwin": [ + "/Applications/Nuke14.0v4/NukeX14.0v4.app" + ], + "linux": [ + "/usr/local/Nuke14.0v4/Nuke14.0" + ] + }, + "arguments": { + "windows": [ + "--nukex" + ], + "darwin": [], + "linux": [ + "--nukex" + ] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-2", + "label": "13.2", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v5\\Nuke13.2.exe" + ], + "darwin": [ + "/Applications/Nuke13.2v5/NukeX13.2v5.app" + ], + "linux": [ + "/usr/local/Nuke13.2v5/Nuke13.2" + ] + }, + "arguments": { + "windows": [ + "--nukex" + ], + "darwin": [], + "linux": [ + "--nukex" + ] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-0", + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe" + ], + "darwin": [ + "/Applications/Nuke13.0v1/NukeX13.0v1.app" + ], + "linux": [ + "/usr/local/Nuke13.0v1/Nuke13.0" + ] + }, + "arguments": { + "windows": [ + "--nukex" + ], + "darwin": [], + "linux": [ + "--nukex" + ] + }, + "environment": "{}" + } + ] + }, + "nukestudio": { + "enabled": true, + "label": "Nuke Studio", + "icon": "{}/app_icons/nukestudio.png", + "host_name": "hiero", + "environment": "{\n \"WORKFILES_STARTUP\": \"0\",\n \"TAG_ASSETBUILD_STARTUP\": \"0\"\n}", + "variants": [ + { + "name": "14-0", + "label": "14.0", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke14.0v4\\Nuke14.0.exe" + ], + "darwin": [ + "/Applications/Nuke14.0v4/NukeStudio14.0v4.app" + ], + "linux": [ + "/usr/local/Nuke14.0v4/Nuke14.0" + ] + }, + "arguments": { + "windows": [ + "--studio" + ], + "darwin": [], + "linux": [ + "--studio" + ] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-2", + "label": "13.2", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v5\\Nuke13.2.exe" + ], + "darwin": [ + "/Applications/Nuke13.2v5/NukeStudio13.2v5.app" + ], + "linux": [ + "/usr/local/Nuke13.2v5/Nuke13.2" + ] + }, + "arguments": { + "windows": [ + "--studio" + ], + "darwin": [], + "linux": [ + "--studio" + ] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-0", + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe" + ], + "darwin": [ + "/Applications/Nuke13.0v1/NukeStudio13.0v1.app" + ], + "linux": [ + "/usr/local/Nuke13.0v1/Nuke13.0" + ] + }, + "arguments": { + "windows": [ + "--studio" + ], + "darwin": [], + "linux": [ + "--studio" + ] + }, + "environment": "{}" + } + ] + }, + "hiero": { + "enabled": true, + "label": "Hiero", + "icon": "{}/app_icons/hiero.png", + "host_name": "hiero", + "environment": "{\n \"WORKFILES_STARTUP\": \"0\",\n \"TAG_ASSETBUILD_STARTUP\": \"0\"\n}", + "variants": [ + { + "name": "14-0", + "label": "14.0", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke14.0v4\\Nuke14.0.exe" + ], + "darwin": [ + "/Applications/Nuke14.0v4/Hiero14.0v4.app" + ], + "linux": [ + "/usr/local/Nuke14.0v4/Nuke14.0" + ] + }, + "arguments": { + "windows": [ + "--hiero" + ], + "darwin": [], + "linux": [ + "--hiero" + ] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-2", + "label": "13.2", + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v5\\Nuke13.2.exe" + ], + "darwin": [ + "/Applications/Nuke13.2v5/Hiero13.2v5.app" + ], + "linux": [ + "/usr/local/Nuke13.2v5/Nuke13.2" + ] + }, + "arguments": { + "windows": [ + "--hiero" + ], + "darwin": [], + "linux": [ + "--hiero" + ] + }, + "environment": "{}", + "use_python_2": false + }, + { + "name": "13-0", + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe" + ], + "darwin": [ + "/Applications/Nuke13.0v1/Hiero13.0v1.app" + ], + "linux": [ + "/usr/local/Nuke13.0v1/Nuke13.0" + ] + }, + "arguments": { + "windows": [ + "--hiero" + ], + "darwin": [], + "linux": [ + "--hiero" + ] + }, + "environment": "{}" + } + ] + }, + "fusion": { + "enabled": true, + "label": "Fusion", + "icon": "{}/app_icons/fusion.png", + "host_name": "fusion", + "environment": "{\n \"FUSION_PYTHON3_HOME\": {\n \"windows\": \"{LOCALAPPDATA}/Programs/Python/Python36\",\n \"darwin\": \"~/Library/Python/3.6/bin\",\n \"linux\": \"/opt/Python/3.6/bin\"\n }\n}", + "variants": [ + { + "name": "17", + "label": "17", + "executables": { + "windows": [ + "C:\\Program Files\\Blackmagic Design\\Fusion 17\\Fusion.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + }, + { + "name": "16", + "label": "16", + "executables": { + "windows": [ + "C:\\Program Files\\Blackmagic Design\\Fusion 16\\Fusion.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + }, + { + "name": "9", + "label": "9", + "executables": { + "windows": [ + "C:\\Program Files\\Blackmagic Design\\Fusion 9\\Fusion.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, + "resolve": { + "enabled": true, + "label": "Resolve", + "icon": "{}/app_icons/resolve.png", + "host_name": "resolve", + "environment": "{\n \"RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR\": [],\n \"RESOLVE_PYTHON3_HOME\": {\n \"windows\": \"{LOCALAPPDATA}/Programs/Python/Python36\",\n \"darwin\": \"~/Library/Python/3.6/bin\",\n \"linux\": \"/opt/Python/3.6/bin\"\n }\n}", + "variants": [ + { + "name": "stable", + "label": "stable", + "executables": { + "windows": [ + "C:/Program Files/Blackmagic Design/DaVinci Resolve/Resolve.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, + "houdini": { + "enabled": true, + "label": "Houdini", + "icon": "{}/app_icons/houdini.png", + "host_name": "houdini", + "environment": "{}", + "variants": [ + { + "name": "18-5", + "label": "18.5", + "executables": { + "windows": [ + "C:\\Program Files\\Side Effects Software\\Houdini 18.5.499\\bin\\houdini.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}", + "use_python_2": true + }, + { + "name": "18", + "label": "18", + "executables": { + "windows": [], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}", + "use_python_2": true + }, + { + "name": "17", + "label": "17", + "executables": { + "windows": [], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}", + "use_python_2": true + } + ] + }, + "blender": { + "enabled": true, + "label": "Blender", + "icon": "{}/app_icons/blender.png", + "host_name": "blender", + "environment": "{}", + "variants": [ + { + "name": "2-83", + "label": "2.83", + "executables": { + "windows": [ + "C:\\Program Files\\Blender Foundation\\Blender 2.83\\blender.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [ + "--python-use-system-env" + ], + "darwin": [ + "--python-use-system-env" + ], + "linux": [ + "--python-use-system-env" + ] + }, + "environment": "{}" + }, + { + "name": "2-90", + "label": "2.90", + "executables": { + "windows": [ + "C:\\Program Files\\Blender Foundation\\Blender 2.90\\blender.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [ + "--python-use-system-env" + ], + "darwin": [ + "--python-use-system-env" + ], + "linux": [ + "--python-use-system-env" + ] + }, + "environment": "{}" + }, + { + "name": "2-91", + "label": "2.91", + "executables": { + "windows": [ + "C:\\Program Files\\Blender Foundation\\Blender 2.91\\blender.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [ + "--python-use-system-env" + ], + "darwin": [ + "--python-use-system-env" + ], + "linux": [ + "--python-use-system-env" + ] + }, + "environment": "{}" + } + ] + }, + "harmony": { + "enabled": true, + "label": "Harmony", + "icon": "{}/app_icons/harmony.png", + "host_name": "harmony", + "environment": "{\n \"AVALON_HARMONY_WORKFILES_ON_LAUNCH\": \"1\"\n}", + "variants": [ + { + "name": "21", + "label": "21", + "executables": { + "windows": [ + "c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 21 Premium\\win64\\bin\\HarmonyPremium.exe" + ], + "darwin": [ + "/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium" + ], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + }, + { + "name": "20", + "label": "20", + "executables": { + "windows": [ + "c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 20 Premium\\win64\\bin\\HarmonyPremium.exe" + ], + "darwin": [ + "/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium" + ], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + }, + { + "name": "17", + "label": "17", + "executables": { + "windows": [ + "c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 17 Premium\\win64\\bin\\HarmonyPremium.exe" + ], + "darwin": [ + "/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium" + ], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, + "tvpaint": { + "enabled": true, + "label": "TVPaint", + "icon": "{}/app_icons/tvpaint.png", + "host_name": "tvpaint", + "environment": "{}", + "variants": [ + { + "name": "animation_11-64bits", + "label": "11 (64bits)", + "executables": { + "windows": [ + "C:\\Program Files\\TVPaint Developpement\\TVPaint Animation 11 (64bits)\\TVPaint Animation 11 (64bits).exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + }, + { + "name": "animation_11-32bits", + "label": "11 (32bits)", + "executables": { + "windows": [ + "C:\\Program Files (x86)\\TVPaint Developpement\\TVPaint Animation 11 (32bits)\\TVPaint Animation 11 (32bits).exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, + "photoshop": { + "enabled": true, + "label": "Photoshop", + "icon": "{}/app_icons/photoshop.png", + "host_name": "photoshop", + "environment": "{\n \"AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH\": \"1\",\n \"WORKFILES_SAVE_AS\": \"Yes\"\n}", + "variants": [ + { + "name": "2020", + "label": "2020", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + }, + { + "name": "2021", + "label": "2021", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe Photoshop 2021\\Photoshop.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + }, + { + "name": "2022", + "label": "2022", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe Photoshop 2022\\Photoshop.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, + "aftereffects": { + "enabled": true, + "label": "AfterEffects", + "icon": "{}/app_icons/aftereffects.png", + "host_name": "aftereffects", + "environment": "{\n \"AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH\": \"1\",\n \"WORKFILES_SAVE_AS\": \"Yes\"\n}", + "variants": [ + { + "name": "2020", + "label": "2020", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe After Effects 2020\\Support Files\\AfterFX.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + }, + { + "name": "2021", + "label": "2021", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe After Effects 2021\\Support Files\\AfterFX.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + }, + { + "name": "2022", + "label": "2022", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe After Effects 2022\\Support Files\\AfterFX.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"MULTIPROCESS\": \"No\"\n}" + } + ] + }, + "celaction": { + "enabled": true, + "label": "CelAction 2D", + "icon": "app_icons/celaction.png", + "host_name": "celaction", + "environment": "{\n \"CELACTION_TEMPLATE\": \"{OPENPYPE_REPOS_ROOT}/openpype/hosts/celaction/celaction_template_scene.scn\"\n}", + "variants": [ + { + "name": "local", + "label": "local", + "executables": { + "windows": [], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, + "unreal": { + "enabled": true, + "label": "Unreal Editor", + "icon": "{}/app_icons/ue4.png", + "host_name": "unreal", + "environment": "{}", + "variants": [ + { + "name": "4-26", + "label": "4.26", + "executables": {}, + "arguments": {}, + "environment": "{}" + } + ] + }, + "djvview": { + "enabled": true, + "label": "DJV View", + "icon": "{}/app_icons/djvView.png", + "host_name": "", + "environment": "{}", + "variants": [ + { + "name": "1-1", + "label": "1.1", + "executables": { + "windows": [], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, + "additional_apps": [] + } +} diff --git a/server_addon/applications/server/settings.py b/server_addon/applications/server/settings.py new file mode 100644 index 0000000000..fd481b6ce8 --- /dev/null +++ b/server_addon/applications/server/settings.py @@ -0,0 +1,201 @@ +import json +from pydantic import Field, validator + +from ayon_server.settings import BaseSettingsModel, ensure_unique_names +from ayon_server.exceptions import BadRequestException + + +def validate_json_dict(value): + if not value.strip(): + return "{}" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, dict) + except json.JSONDecodeError as exc: + print(exc) + success = False + + if not success: + raise BadRequestException( + "Environment's can't be parsed as json object" + ) + return value + + +class MultiplatformStrList(BaseSettingsModel): + windows: list[str] = Field(default_factory=list, title="Windows") + linux: list[str] = Field(default_factory=list, title="Linux") + darwin: list[str] = Field(default_factory=list, title="MacOS") + + +class AppVariant(BaseSettingsModel): + name: str = Field("", title="Name") + label: str = Field("", title="Label") + executables: MultiplatformStrList = Field( + default_factory=MultiplatformStrList, title="Executables" + ) + arguments: MultiplatformStrList = Field( + default_factory=MultiplatformStrList, title="Arguments" + ) + environment: str = Field("{}", title="Environment", widget="textarea") + + @validator("environment") + def validate_json(cls, value): + return validate_json_dict(value) + + +class AppVariantWithPython(AppVariant): + use_python_2: bool = Field(False, title="Use Python 2") + + +class AppGroup(BaseSettingsModel): + enabled: bool = Field(True) + label: str = Field("", title="Label") + host_name: str = Field("", title="Host name") + icon: str = Field("", title="Icon") + environment: str = Field("{}", title="Environment", widget="textarea") + + variants: list[AppVariant] = Field( + default_factory=list, + title="Variants", + description="Different variants of the applications", + section="Variants", + ) + + @validator("variants") + def validate_unique_name(cls, value): + ensure_unique_names(value) + return value + + +class AppGroupWithPython(AppGroup): + variants: list[AppVariantWithPython] = Field( + default_factory=list, + title="Variants", + description="Different variants of the applications", + section="Variants", + ) + + +class AdditionalAppGroup(BaseSettingsModel): + enabled: bool = Field(True) + name: str = Field("", title="Name") + label: str = Field("", title="Label") + host_name: str = Field("", title="Host name") + icon: str = Field("", title="Icon") + environment: str = Field("{}", title="Environment", widget="textarea") + + variants: list[AppVariantWithPython] = Field( + default_factory=list, + title="Variants", + description="Different variants of the applications", + section="Variants", + ) + + @validator("variants") + def validate_unique_name(cls, value): + ensure_unique_names(value) + return value + + +class ToolVariantModel(BaseSettingsModel): + name: str = Field("", title="Name") + label: str = Field("", title="Label") + host_names: list[str] = Field(default_factory=list, title="Hosts") + # TODO use applications enum if possible + app_variants: list[str] = Field(default_factory=list, title="Applications") + environment: str = Field("{}", title="Environments", widget="textarea") + + @validator("environment") + def validate_json(cls, value): + return validate_json_dict(value) + + +class ToolGroupModel(BaseSettingsModel): + name: str = Field("", title="Name") + label: str = Field("", title="Label") + environment: str = Field("{}", title="Environments", widget="textarea") + variants: list[ToolVariantModel] = Field( + default_factory=ToolVariantModel + ) + + @validator("environment") + def validate_json(cls, value): + return validate_json_dict(value) + + @validator("variants") + def validate_unique_name(cls, value): + ensure_unique_names(value) + return value + + +class ApplicationsSettings(BaseSettingsModel): + """Applications settings""" + + maya: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Autodesk Maya") + adsk_3dsmax: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Autodesk 3ds Max") + flame: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Autodesk Flame") + nuke: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Nuke") + nukeassist: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Nuke Assist") + nukex: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Nuke X") + nukestudio: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Nuke Studio") + hiero: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Hiero") + fusion: AppGroup = Field( + default_factory=AppGroupWithPython, title="Fusion") + resolve: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Resolve") + houdini: AppGroupWithPython = Field( + default_factory=AppGroupWithPython, title="Houdini") + blender: AppGroup = Field( + default_factory=AppGroupWithPython, title="Blender") + harmony: AppGroup = Field( + default_factory=AppGroupWithPython, title="Harmony") + tvpaint: AppGroup = Field( + default_factory=AppGroupWithPython, title="TVPaint") + photoshop: AppGroup = Field( + default_factory=AppGroupWithPython, title="Adobe Photoshop") + aftereffects: AppGroup = Field( + default_factory=AppGroupWithPython, title="Adobe After Effects") + celaction: AppGroup = Field( + default_factory=AppGroupWithPython, title="Celaction 2D") + unreal: AppGroup = Field( + default_factory=AppGroupWithPython, title="Unreal Editor") + additional_apps: list[AdditionalAppGroup] = Field( + default_factory=list, title="Additional Applications") + + @validator("additional_apps") + def validate_unique_name(cls, value): + ensure_unique_names(value) + return value + + +class ApplicationsAddonSettings(BaseSettingsModel): + applications: ApplicationsSettings = Field( + default_factory=ApplicationsSettings, + title="Applications", + scope=["studio"] + ) + tool_groups: list[ToolGroupModel] = Field( + default_factory=list, + scope=["studio"] + ) + only_available: bool = Field( + True, title="Show only available applications") + + @validator("tool_groups") + def validate_unique_name(cls, value): + ensure_unique_names(value) + return value + + +DEFAULT_VALUES = { + "only_available": False +} diff --git a/server_addon/applications/server/tools.json b/server_addon/applications/server/tools.json new file mode 100644 index 0000000000..54bee11cf7 --- /dev/null +++ b/server_addon/applications/server/tools.json @@ -0,0 +1,55 @@ +{ + "tool_groups": [ + { + "environment": "{\n \"MTOA\": \"{STUDIO_SOFTWARE}/arnold/mtoa_{MAYA_VERSION}_{MTOA_VERSION}\",\n \"MAYA_RENDER_DESC_PATH\": \"{MTOA}\",\n \"MAYA_MODULE_PATH\": \"{MTOA}\",\n \"ARNOLD_PLUGIN_PATH\": \"{MTOA}/shaders\",\n \"MTOA_EXTENSIONS_PATH\": {\n \"darwin\": \"{MTOA}/extensions\",\n \"linux\": \"{MTOA}/extensions\",\n \"windows\": \"{MTOA}/extensions\"\n },\n \"MTOA_EXTENSIONS\": {\n \"darwin\": \"{MTOA}/extensions\",\n \"linux\": \"{MTOA}/extensions\",\n \"windows\": \"{MTOA}/extensions\"\n },\n \"DYLD_LIBRARY_PATH\": {\n \"darwin\": \"{MTOA}/bin\"\n },\n \"PATH\": {\n \"windows\": \"{PATH};{MTOA}/bin\"\n }\n}", + "name": "mtoa", + "label": "Autodesk Arnold", + "variants": [ + { + "host_names": [], + "app_variants": [], + "environment": "{\n \"MTOA_VERSION\": \"3.2\"\n}", + "name": "3-2", + "label": "3.2" + }, + { + "host_names": [], + "app_variants": [], + "environment": "{\n \"MTOA_VERSION\": \"3.1\"\n}", + "name": "3-1", + "label": "3.1" + } + ] + }, + { + "environment": "{}", + "name": "vray", + "label": "Chaos Group Vray", + "variants": [] + }, + { + "environment": "{}", + "name": "yeti", + "label": "Peregrine Labs Yeti", + "variants": [] + }, + { + "environment": "{}", + "name": "renderman", + "label": "Pixar Renderman", + "variants": [ + { + "host_names": [ + "maya" + ], + "app_variants": [ + "maya/2022" + ], + "environment": "{\n \"RFMTREE\": {\n \"windows\": \"C:\\\\Program Files\\\\Pixar\\\\RenderManForMaya-24.3\",\n \"darwin\": \"/Applications/Pixar/RenderManForMaya-24.3\",\n \"linux\": \"/opt/pixar/RenderManForMaya-24.3\"\n },\n \"RMANTREE\": {\n \"windows\": \"C:\\\\Program Files\\\\Pixar\\\\RenderManProServer-24.3\",\n \"darwin\": \"/Applications/Pixar/RenderManProServer-24.3\",\n \"linux\": \"/opt/pixar/RenderManProServer-24.3\"\n }\n}", + "name": "24-3-maya", + "label": "24.3 RFM" + } + ] + } + ] +} diff --git a/server_addon/applications/server/version.py b/server_addon/applications/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/applications/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/blender/server/__init__.py b/server_addon/blender/server/__init__.py new file mode 100644 index 0000000000..a7d6cb4400 --- /dev/null +++ b/server_addon/blender/server/__init__.py @@ -0,0 +1,19 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import BlenderSettings, DEFAULT_VALUES + + +class BlenderAddon(BaseServerAddon): + name = "blender" + title = "Blender" + version = __version__ + settings_model: Type[BlenderSettings] = BlenderSettings + frontend_scopes = {} + services = {} + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/blender/server/settings/__init__.py b/server_addon/blender/server/settings/__init__.py new file mode 100644 index 0000000000..3d51e5c3e1 --- /dev/null +++ b/server_addon/blender/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + BlenderSettings, + DEFAULT_VALUES, +) + + +__all__ = ( + "BlenderSettings", + "DEFAULT_VALUES", +) diff --git a/server_addon/blender/server/settings/imageio.py b/server_addon/blender/server/settings/imageio.py new file mode 100644 index 0000000000..a6d3c5ff64 --- /dev/null +++ b/server_addon/blender/server/settings/imageio.py @@ -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 BlenderImageIOModel(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" + ) diff --git a/server_addon/blender/server/settings/main.py b/server_addon/blender/server/settings/main.py new file mode 100644 index 0000000000..ec969afa93 --- /dev/null +++ b/server_addon/blender/server/settings/main.py @@ -0,0 +1,53 @@ +from pydantic import Field +from ayon_server.settings import ( + BaseSettingsModel, + TemplateWorkfileBaseOptions, +) + +from .imageio import BlenderImageIOModel +from .publish_plugins import ( + PublishPuginsModel, + DEFAULT_BLENDER_PUBLISH_SETTINGS +) + + +class UnitScaleSettingsModel(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + apply_on_opening: bool = Field( + False, title="Apply on Opening Existing Files") + base_file_unit_scale: float = Field( + 1.0, title="Base File Unit Scale" + ) + + +class BlenderSettings(BaseSettingsModel): + unit_scale_settings: UnitScaleSettingsModel = Field( + default_factory=UnitScaleSettingsModel, + title="Set Unit Scale" + ) + imageio: BlenderImageIOModel = Field( + default_factory=BlenderImageIOModel, + title="Color Management (ImageIO)" + ) + workfile_builder: TemplateWorkfileBaseOptions = Field( + default_factory=TemplateWorkfileBaseOptions, + title="Workfile Builder" + ) + publish: PublishPuginsModel = Field( + default_factory=PublishPuginsModel, + title="Publish Plugins" + ) + + +DEFAULT_VALUES = { + "unit_scale_settings": { + "enabled": True, + "apply_on_opening": False, + "base_file_unit_scale": 0.01 + }, + "publish": DEFAULT_BLENDER_PUBLISH_SETTINGS, + "workfile_builder": { + "create_first_version": False, + "custom_templates": [] + } +} diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py new file mode 100644 index 0000000000..43ed3e3d0d --- /dev/null +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -0,0 +1,273 @@ +import json +from pydantic import Field, validator +from ayon_server.exceptions import BadRequestException +from ayon_server.settings import BaseSettingsModel + + +def validate_json_dict(value): + if not value.strip(): + return "{}" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, dict) + except json.JSONDecodeError: + success = False + + if not success: + raise BadRequestException( + "Environment's can't be parsed as json object" + ) + return value + + +class ValidatePluginModel(BaseSettingsModel): + enabled: bool = Field(True) + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + + +class ExtractBlendModel(BaseSettingsModel): + enabled: bool = Field(True) + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + families: list[str] = Field( + default_factory=list, + title="Families" + ) + + +class ExtractPlayblastModel(BaseSettingsModel): + enabled: bool = Field(True) + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + presets: str = Field("", title="Presets", widget="textarea") + + @validator("presets") + def validate_json(cls, value): + return validate_json_dict(value) + + +class PublishPuginsModel(BaseSettingsModel): + ValidateCameraZeroKeyframe: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Camera Zero Keyframe", + section="Validators" + ) + ValidateMeshHasUvs: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Mesh Has Uvs" + ) + ValidateMeshNoNegativeScale: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Mesh No Negative Scale" + ) + ValidateTransformZero: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Transform Zero" + ) + ValidateNoColonsInName: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate No Colons In Name" + ) + ExtractBlend: ExtractBlendModel = Field( + default_factory=ExtractBlendModel, + title="Extract Blend", + section="Extractors" + ) + ExtractFBX: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Extract FBX" + ) + ExtractABC: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Extract ABC" + ) + ExtractBlendAnimation: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Extract Blend Animation" + ) + ExtractAnimationFBX: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Extract Animation FBX" + ) + ExtractCamera: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Extract Camera" + ) + ExtractLayout: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Extract Layout" + ) + ExtractThumbnail: ExtractPlayblastModel = Field( + default_factory=ExtractPlayblastModel, + title="Extract Thumbnail" + ) + ExtractPlayblast: ExtractPlayblastModel = Field( + default_factory=ExtractPlayblastModel, + title="Extract Playblast" + ) + + +DEFAULT_BLENDER_PUBLISH_SETTINGS = { + "ValidateCameraZeroKeyframe": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshHasUvs": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshNoNegativeScale": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateTransformZero": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateNoColonsInName": { + "enabled": True, + "optional": False, + "active": True + }, + "ExtractBlend": { + "enabled": True, + "optional": True, + "active": True, + "families": [ + "model", + "camera", + "rig", + "action", + "layout" + ] + }, + "ExtractFBX": { + "enabled": True, + "optional": True, + "active": False + }, + "ExtractABC": { + "enabled": True, + "optional": True, + "active": False + }, + "ExtractBlendAnimation": { + "enabled": True, + "optional": True, + "active": True + }, + "ExtractAnimationFBX": { + "enabled": True, + "optional": True, + "active": False + }, + "ExtractCamera": { + "enabled": True, + "optional": True, + "active": True + }, + "ExtractLayout": { + "enabled": True, + "optional": True, + "active": False + }, + "ExtractThumbnail": { + "enabled": True, + "optional": True, + "active": True, + "presets": json.dumps( + { + "model": { + "image_settings": { + "file_format": "JPEG", + "color_mode": "RGB", + "quality": 100 + }, + "display_options": { + "shading": { + "light": "STUDIO", + "studio_light": "Default", + "type": "SOLID", + "color_type": "OBJECT", + "show_xray": False, + "show_shadows": False, + "show_cavity": True + }, + "overlay": { + "show_overlays": False + } + } + }, + "rig": { + "image_settings": { + "file_format": "JPEG", + "color_mode": "RGB", + "quality": 100 + }, + "display_options": { + "shading": { + "light": "STUDIO", + "studio_light": "Default", + "type": "SOLID", + "color_type": "OBJECT", + "show_xray": True, + "show_shadows": False, + "show_cavity": False + }, + "overlay": { + "show_overlays": True, + "show_ortho_grid": False, + "show_floor": False, + "show_axis_x": False, + "show_axis_y": False, + "show_axis_z": False, + "show_text": False, + "show_stats": False, + "show_cursor": False, + "show_annotation": False, + "show_extras": False, + "show_relationship_lines": False, + "show_outline_selected": False, + "show_motion_paths": False, + "show_object_origins": False, + "show_bones": True + } + } + } + }, + indent=4, + ) + }, + "ExtractPlayblast": { + "enabled": True, + "optional": True, + "active": True, + "presets": json.dumps( + { + "default": { + "image_settings": { + "file_format": "PNG", + "color_mode": "RGB", + "color_depth": "8", + "compression": 15 + }, + "display_options": { + "shading": { + "type": "MATERIAL", + "render_pass": "COMBINED" + }, + "overlay": { + "show_overlays": False + } + } + } + }, + indent=4 + ) + } +} diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/blender/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/celaction/server/__init__.py b/server_addon/celaction/server/__init__.py new file mode 100644 index 0000000000..90d3dbaa01 --- /dev/null +++ b/server_addon/celaction/server/__init__.py @@ -0,0 +1,19 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import CelActionSettings, DEFAULT_VALUES + + +class CelActionAddon(BaseServerAddon): + name = "celaction" + title = "CelAction" + version = __version__ + settings_model: Type[CelActionSettings] = CelActionSettings + frontend_scopes = {} + services = {} + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/celaction/server/imageio.py b/server_addon/celaction/server/imageio.py new file mode 100644 index 0000000000..72da441528 --- /dev/null +++ b/server_addon/celaction/server/imageio.py @@ -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 CelActionImageIOModel(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" + ) diff --git a/server_addon/celaction/server/settings.py b/server_addon/celaction/server/settings.py new file mode 100644 index 0000000000..68d1d2dc31 --- /dev/null +++ b/server_addon/celaction/server/settings.py @@ -0,0 +1,92 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel +from .imageio import CelActionImageIOModel + + +class CollectRenderPathModel(BaseSettingsModel): + output_extension: str = Field( + "", + title="Output render file extension" + ) + anatomy_template_key_render_files: str = Field( + "", + title="Anatomy template key: render files" + ) + anatomy_template_key_metadata: str = Field( + "", + title="Anatomy template key: metadata job file" + ) + + +def _workfile_submit_overrides(): + return [ + { + "value": "render_chunk", + "label": "Pass chunk size" + }, + { + "value": "frame_range", + "label": "Pass frame range" + }, + { + "value": "resolution", + "label": "Pass resolution" + } + ] + + +class WorkfileModel(BaseSettingsModel): + submission_overrides: list[str] = Field( + default_factory=list, + title="Submission workfile overrides", + enum_resolver=_workfile_submit_overrides + ) + + +class PublishPuginsModel(BaseSettingsModel): + CollectRenderPath: CollectRenderPathModel = Field( + default_factory=CollectRenderPathModel, + title="Collect Render Path" + ) + + +class CelActionSettings(BaseSettingsModel): + imageio: CelActionImageIOModel = Field( + default_factory=CelActionImageIOModel, + title="Color Management (ImageIO)" + ) + workfile: WorkfileModel = Field( + title="Workfile" + ) + publish: PublishPuginsModel = Field( + default_factory=PublishPuginsModel, + title="Publish plugins", + ) + + +DEFAULT_VALUES = { + "imageio": { + "ocio_config": { + "enabled": False, + "filepath": [] + }, + "file_rules": { + "enabled": False, + "rules": [] + } + }, + "workfile": { + "submission_overrides": [ + "render_chunk", + "frame_range", + "resolution" + ] + }, + "publish": { + "CollectRenderPath": { + "output_extension": "png", + "anatomy_template_key_render_files": "render", + "anatomy_template_key_metadata": "render" + } + } +} diff --git a/server_addon/celaction/server/version.py b/server_addon/celaction/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/celaction/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/clockify/server/__init__.py b/server_addon/clockify/server/__init__.py new file mode 100644 index 0000000000..0fa453fdf4 --- /dev/null +++ b/server_addon/clockify/server/__init__.py @@ -0,0 +1,15 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import ClockifySettings + + +class ClockifyAddon(BaseServerAddon): + name = "clockify" + title = "Clockify" + version = __version__ + settings_model: Type[ClockifySettings] = ClockifySettings + frontend_scopes = {} + services = {} diff --git a/server_addon/clockify/server/settings.py b/server_addon/clockify/server/settings.py new file mode 100644 index 0000000000..f6891fc5b8 --- /dev/null +++ b/server_addon/clockify/server/settings.py @@ -0,0 +1,9 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class ClockifySettings(BaseSettingsModel): + workspace_name: str = Field( + "", + title="Workspace name" + ) diff --git a/server_addon/clockify/server/version.py b/server_addon/clockify/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/clockify/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/core/server/__init__.py b/server_addon/core/server/__init__.py new file mode 100644 index 0000000000..ff91f91c75 --- /dev/null +++ b/server_addon/core/server/__init__.py @@ -0,0 +1,14 @@ +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import CoreSettings, DEFAULT_VALUES + + +class CoreAddon(BaseServerAddon): + name = "core" + version = __version__ + settings_model = CoreSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/core/server/settings/__init__.py b/server_addon/core/server/settings/__init__.py new file mode 100644 index 0000000000..527a2bdc0c --- /dev/null +++ b/server_addon/core/server/settings/__init__.py @@ -0,0 +1,7 @@ +from .main import CoreSettings, DEFAULT_VALUES + + +__all__ = ( + "CoreSettings", + "DEFAULT_VALUES", +) diff --git a/server_addon/core/server/settings/main.py b/server_addon/core/server/settings/main.py new file mode 100644 index 0000000000..a1a86ae0a5 --- /dev/null +++ b/server_addon/core/server/settings/main.py @@ -0,0 +1,160 @@ +import json +from pydantic import Field, validator +from ayon_server.settings import ( + BaseSettingsModel, + MultiplatformPathListModel, + ensure_unique_names, +) +from ayon_server.exceptions import BadRequestException + +from .publish_plugins import PublishPuginsModel, DEFAULT_PUBLISH_VALUES +from .tools import GlobalToolsModel, DEFAULT_TOOLS_VALUES + + +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 CoreImageIOFileRulesModel(BaseSettingsModel): + activate_global_file_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 CoreImageIOConfigModel(BaseSettingsModel): + filepath: list[str] = Field(default_factory=list, title="Config path") + + +class CoreImageIOBaseModel(BaseSettingsModel): + activate_global_color_management: bool = Field( + False, + title="Override global OCIO config" + ) + ocio_config: CoreImageIOConfigModel = Field( + default_factory=CoreImageIOConfigModel, title="OCIO config" + ) + file_rules: CoreImageIOFileRulesModel = Field( + default_factory=CoreImageIOFileRulesModel, title="File Rules" + ) + + +class CoreSettings(BaseSettingsModel): + studio_name: str = Field("", title="Studio name") + studio_code: str = Field("", title="Studio code") + environments: str = Field( + "{}", + title="Global environment variables", + widget="textarea", + scope=["studio"], + ) + tools: GlobalToolsModel = Field( + default_factory=GlobalToolsModel, + title="Tools" + ) + imageio: CoreImageIOBaseModel = Field( + default_factory=CoreImageIOBaseModel, + title="Color Management (ImageIO)" + ) + publish: PublishPuginsModel = Field( + default_factory=PublishPuginsModel, + title="Publish plugins" + ) + project_plugins: MultiplatformPathListModel = Field( + default_factory=MultiplatformPathListModel, + title="Additional Project Plugin Paths", + ) + project_folder_structure: str = Field( + "{}", + widget="textarea", + title="Project folder structure", + section="---" + ) + project_environments: str = Field( + "{}", + widget="textarea", + title="Project environments", + section="---" + ) + + @validator( + "environments", + "project_folder_structure", + "project_environments") + def validate_json(cls, value): + if not value.strip(): + return "{}" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, dict) + except json.JSONDecodeError: + success = False + + if not success: + raise BadRequestException( + "Environment's can't be parsed as json object" + ) + return value + + +DEFAULT_VALUES = { + "imageio": { + "activate_global_color_management": False, + "ocio_config": { + "filepath": [ + "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", + "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio" + ] + }, + "file_rules": { + "activate_global_file_rules": False, + "rules": [ + { + "name": "example", + "pattern": ".*(beauty).*", + "colorspace": "ACES - ACEScg", + "ext": "exr" + } + ] + } + }, + "studio_name": "", + "studio_code": "", + "environments": "{}", + "tools": DEFAULT_TOOLS_VALUES, + "publish": DEFAULT_PUBLISH_VALUES, + "project_folder_structure": json.dumps({ + "__project_root__": { + "prod": {}, + "resources": { + "footage": { + "plates": {}, + "offline": {} + }, + "audio": {}, + "art_dept": {} + }, + "editorial": {}, + "assets": { + "characters": {}, + "locations": {} + }, + "shots": {} + } + }, indent=4), + "project_plugins": { + "windows": [], + "darwin": [], + "linux": [] + }, + "project_environments": "{}" +} diff --git a/server_addon/core/server/settings/publish_plugins.py b/server_addon/core/server/settings/publish_plugins.py new file mode 100644 index 0000000000..c012312579 --- /dev/null +++ b/server_addon/core/server/settings/publish_plugins.py @@ -0,0 +1,959 @@ +from pydantic import Field, validator + +from ayon_server.settings import ( + BaseSettingsModel, + MultiplatformPathModel, + normalize_name, + ensure_unique_names, + task_types_enum, +) + +from ayon_server.types import ColorRGBA_uint8 + + +class ValidateBaseModel(BaseSettingsModel): + _isGroup = True + enabled: bool = Field(True) + optional: bool = Field(True, title="Optional") + active: bool = Field(True, title="Active") + + +class CollectAnatomyInstanceDataModel(BaseSettingsModel): + _isGroup = True + follow_workfile_version: bool = Field( + True, title="Collect Anatomy Instance Data" + ) + + +class CollectAudioModel(BaseSettingsModel): + _isGroup = True + enabled: bool = Field(True) + audio_product_name: str = Field( + "", title="Name of audio variant" + ) + + +class CollectSceneVersionModel(BaseSettingsModel): + _isGroup = True + hosts: list[str] = Field( + default_factory=list, + title="Host names" + ) + skip_hosts_headless_publish: list[str] = Field( + default_factory=list, + title="Skip for host if headless publish" + ) + + +class CollectCommentPIModel(BaseSettingsModel): + enabled: bool = Field(True) + families: list[str] = Field(default_factory=list, title="Families") + + +class CollectFramesFixDefModel(BaseSettingsModel): + enabled: bool = Field(True) + rewrite_version_enable: bool = Field( + True, + title="Show 'Rewrite latest version' toggle" + ) + + +class ValidateIntentProfile(BaseSettingsModel): + _layout = "expanded" + hosts: list[str] = Field(default_factory=list, title="Host names") + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + tasks: list[str] = Field(default_factory=list, title="Task names") + # TODO This was 'validate' in v3 + validate_intent: bool = Field(True, title="Validate") + + +class ValidateIntentModel(BaseSettingsModel): + """Validate if Publishing intent was selected. + + It is possible to disable validation for specific publishing context + with profiles. + """ + + _isGroup = True + enabled: bool = Field(False) + profiles: list[ValidateIntentProfile] = Field(default_factory=list) + + +class ExtractThumbnailFFmpegModel(BaseSettingsModel): + _layout = "expanded" + input: list[str] = Field( + default_factory=list, + title="FFmpeg input arguments" + ) + output: list[str] = Field( + default_factory=list, + title="FFmpeg input arguments" + ) + + +class ExtractThumbnailModel(BaseSettingsModel): + _isGroup = True + enabled: bool = Field(True) + ffmpeg_args: ExtractThumbnailFFmpegModel = Field( + default_factory=ExtractThumbnailFFmpegModel + ) + + +def _extract_oiio_transcoding_type(): + return [ + {"value": "colorspace", "label": "Use Colorspace"}, + {"value": "display", "label": "Use Display&View"} + ] + + +class OIIOToolArgumentsModel(BaseSettingsModel): + additional_command_args: list[str] = Field( + default_factory=list, title="Arguments") + + +class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): + extension: str = Field("", title="Extension") + transcoding_type: str = Field( + "colorspace", + title="Transcoding type", + enum_resolver=_extract_oiio_transcoding_type + ) + colorspace: str = Field("", title="Colorspace") + display: str = Field("", title="Display") + view: str = Field("", title="View") + oiiotool_args: OIIOToolArgumentsModel = Field( + default_factory=OIIOToolArgumentsModel, + title="OIIOtool arguments") + + tags: list[str] = Field(default_factory=list, title="Tags") + custom_tags: list[str] = Field(default_factory=list, title="Custom Tags") + + +class ExtractOIIOTranscodeProfileModel(BaseSettingsModel): + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + hosts: list[str] = Field( + default_factory=list, + title="Host names" + ) + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field( + default_factory=list, + title="Task names" + ) + product_names: list[str] = Field( + default_factory=list, + title="Product names" + ) + delete_original: bool = Field( + True, + title="Delete Original Representation" + ) + outputs: list[ExtractOIIOTranscodeOutputModel] = Field( + default_factory=list, + title="Output Definitions", + ) + + +class ExtractOIIOTranscodeModel(BaseSettingsModel): + enabled: bool = Field(True) + profiles: list[ExtractOIIOTranscodeProfileModel] = Field( + default_factory=list, title="Profiles" + ) + + +# --- [START] Extract Review --- +class ExtractReviewFFmpegModel(BaseSettingsModel): + video_filters: list[str] = Field( + default_factory=list, + title="Video filters" + ) + audio_filters: list[str] = Field( + default_factory=list, + title="Audio filters" + ) + input: list[str] = Field( + default_factory=list, + title="Input arguments" + ) + output: list[str] = Field( + default_factory=list, + title="Output arguments" + ) + + +def extract_review_filter_enum(): + return [ + { + "value": "everytime", + "label": "Always" + }, + { + "value": "single_frame", + "label": "Only if input has 1 image frame" + }, + { + "value": "multi_frame", + "label": "Only if input is video or sequence of frames" + } + ] + + +class ExtractReviewFilterModel(BaseSettingsModel): + families: list[str] = Field(default_factory=list, title="Families") + product_names: list[str] = Field( + default_factory=list, title="Product names") + custom_tags: list[str] = Field(default_factory=list, title="Custom Tags") + single_frame_filter: str = Field( + "everytime", + description=( + "Use output always / only if input is 1 frame" + " image / only if has 2+ frames or is video" + ), + enum_resolver=extract_review_filter_enum + ) + + +class ExtractReviewLetterBox(BaseSettingsModel): + enabled: bool = Field(True) + ratio: float = Field( + 0.0, + title="Ratio", + ge=0.0, + le=10000.0 + ) + fill_color: ColorRGBA_uint8 = Field( + (0, 0, 0, 0.0), + title="Fill Color" + ) + line_thickness: int = Field( + 0, + title="Line Thickness", + ge=0, + le=1000 + ) + line_color: ColorRGBA_uint8 = Field( + (0, 0, 0, 0.0), + title="Line Color" + ) + + +class ExtractReviewOutputDefModel(BaseSettingsModel): + _layout = "expanded" + name: str = Field("", title="Name") + ext: str = Field("", title="Output extension") + # TODO use some different source of tags + tags: list[str] = Field(default_factory=list, title="Tags") + burnins: list[str] = Field( + default_factory=list, title="Link to a burnin by name" + ) + ffmpeg_args: ExtractReviewFFmpegModel = Field( + default_factory=ExtractReviewFFmpegModel, + title="FFmpeg arguments" + ) + filter: ExtractReviewFilterModel = Field( + default_factory=ExtractReviewFilterModel, + title="Additional output filtering" + ) + overscan_crop: str = Field( + "", + title="Overscan crop", + description=( + "Crop input overscan. See the documentation for more information." + ) + ) + overscan_color: ColorRGBA_uint8 = Field( + (0, 0, 0, 0.0), + title="Overscan color", + description=( + "Overscan color is used when input aspect ratio is not" + " same as output aspect ratio." + ) + ) + width: int = Field( + 0, + ge=0, + le=100000, + title="Output width", + description=( + "Width and Height must be both set to higher" + " value than 0 else source resolution is used." + ) + ) + height: int = Field( + 0, + title="Output height", + ge=0, + le=100000, + ) + scale_pixel_aspect: bool = Field( + True, + title="Scale pixel aspect", + description=( + "Rescale input when it's pixel aspect ratio is not 1." + " Usefull for anamorph reviews." + ) + ) + bg_color: ColorRGBA_uint8 = Field( + (0, 0, 0, 0.0), + description=( + "Background color is used only when input have transparency" + " and Alpha is higher than 0." + ), + title="Background color", + ) + letter_box: ExtractReviewLetterBox = Field( + default_factory=ExtractReviewLetterBox, + title="Letter Box" + ) + + @validator("name") + def validate_name(cls, value): + """Ensure name does not contain weird characters""" + return normalize_name(value) + + +class ExtractReviewProfileModel(BaseSettingsModel): + _layout = "expanded" + product_types: list[str] = Field( + default_factory=list, title="Product types" + ) + # TODO use hosts enum + hosts: list[str] = Field( + default_factory=list, title="Host names" + ) + outputs: list[ExtractReviewOutputDefModel] = Field( + default_factory=list, title="Output Definitions" + ) + + @validator("outputs") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class ExtractReviewModel(BaseSettingsModel): + _isGroup = True + enabled: bool = Field(True) + profiles: list[ExtractReviewProfileModel] = Field( + default_factory=list, + title="Profiles" + ) +# --- [END] Extract Review --- + + +# --- [Start] Extract Burnin --- +class ExtractBurninOptionsModel(BaseSettingsModel): + font_size: int = Field(0, ge=0, title="Font size") + font_color: ColorRGBA_uint8 = Field( + (255, 255, 255, 1.0), + title="Font color" + ) + bg_color: ColorRGBA_uint8 = Field( + (0, 0, 0, 1.0), + title="Background color" + ) + x_offset: int = Field(0, title="X Offset") + y_offset: int = Field(0, title="Y Offset") + bg_padding: int = Field(0, title="Padding around text") + font_filepath: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Font file path" + ) + + +class ExtractBurninDefFilter(BaseSettingsModel): + families: list[str] = Field( + default_factory=list, + title="Families" + ) + tags: list[str] = Field( + default_factory=list, + title="Tags" + ) + + +class ExtractBurninDef(BaseSettingsModel): + _isGroup = True + _layout = "expanded" + name: str = Field("") + TOP_LEFT: str = Field("", topic="Top Left") + TOP_CENTERED: str = Field("", topic="Top Centered") + TOP_RIGHT: str = Field("", topic="Top Right") + BOTTOM_LEFT: str = Field("", topic="Bottom Left") + BOTTOM_CENTERED: str = Field("", topic="Bottom Centered") + BOTTOM_RIGHT: str = Field("", topic="Bottom Right") + filter: ExtractBurninDefFilter = Field( + default_factory=ExtractBurninDefFilter, + title="Additional filtering" + ) + + @validator("name") + def validate_name(cls, value): + """Ensure name does not contain weird characters""" + return normalize_name(value) + + +class ExtractBurninProfile(BaseSettingsModel): + _layout = "expanded" + product_types: list[str] = Field( + default_factory=list, + title="Produt types" + ) + hosts: list[str] = Field( + default_factory=list, + title="Host names" + ) + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field( + default_factory=list, + title="Task names" + ) + product_names: list[str] = Field( + default_factory=list, + title="Product names" + ) + burnins: list[ExtractBurninDef] = Field( + default_factory=list, + title="Burnins" + ) + + @validator("burnins") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + + return value + + +class ExtractBurninModel(BaseSettingsModel): + _isGroup = True + enabled: bool = Field(True) + options: ExtractBurninOptionsModel = Field( + default_factory=ExtractBurninOptionsModel, + title="Burnin formatting options" + ) + profiles: list[ExtractBurninProfile] = Field( + default_factory=list, + title="Profiles" + ) +# --- [END] Extract Burnin --- + + +class PreIntegrateThumbnailsProfile(BaseSettingsModel): + _isGroup = True + product_types: list[str] = Field( + default_factory=list, + title="Product types", + ) + hosts: list[str] = Field( + default_factory=list, + title="Hosts", + ) + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + product_names: list[str] = Field( + default_factory=list, + title="Product names", + ) + integrate_thumbnail: bool = Field(True) + + +class PreIntegrateThumbnailsModel(BaseSettingsModel): + """Explicitly set if Thumbnail representation should be integrated. + + If no matching profile set, existing state from Host implementation + is kept. + """ + + _isGroup = True + enabled: bool = Field(True) + integrate_profiles: list[PreIntegrateThumbnailsProfile] = Field( + default_factory=list, + title="Integrate profiles" + ) + + +class IntegrateProductGroupProfile(BaseSettingsModel): + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + hosts: list[str] = Field(default_factory=list, title="Hosts") + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + tasks: list[str] = Field(default_factory=list, title="Task names") + template: str = Field("", title="Template") + + +class IntegrateProductGroupModel(BaseSettingsModel): + """Group published products by filtering logic. + + Set all published instances as a part of specific group named according + to 'Template'. + + Implemented all variants of placeholders '{task}', '{product[type]}', + '{host}', '{product[name]}', '{renderlayer}'. + """ + + _isGroup = True + product_grouping_profiles: list[IntegrateProductGroupProfile] = Field( + default_factory=list, + title="Product group profiles" + ) + + +class IntegrateANProductGroupProfileModel(BaseSettingsModel): + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + hosts: list[str] = Field( + default_factory=list, + title="Hosts" + ) + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + tasks: list[str] = Field( + default_factory=list, + title="Task names" + ) + template: str = Field("", title="Template") + + +class IntegrateANTemplateNameProfileModel(BaseSettingsModel): + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + hosts: list[str] = Field( + default_factory=list, + title="Hosts" + ) + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + tasks: list[str] = Field( + default_factory=list, + title="Task names" + ) + template_name: str = Field("", title="Template name") + + +class IntegrateHeroTemplateNameProfileModel(BaseSettingsModel): + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + hosts: list[str] = Field( + default_factory=list, + title="Hosts" + ) + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field( + default_factory=list, + title="Task names" + ) + template_name: str = Field("", title="Template name") + + +class IntegrateHeroVersionModel(BaseSettingsModel): + _isGroup = True + enabled: bool = Field(True) + optional: bool = Field(False, title="Optional") + active: bool = Field(True, title="Active") + families: list[str] = Field(default_factory=list, title="Families") + # TODO remove when removed from client code + template_name_profiles: list[IntegrateHeroTemplateNameProfileModel] = ( + Field( + default_factory=list, + title="Template name profiles" + ) + ) + + +class CleanUpModel(BaseSettingsModel): + _isGroup = True + paterns: list[str] = Field( + default_factory=list, + title="Patterns (regex)" + ) + remove_temp_renders: bool = Field(False, title="Remove Temp renders") + + +class CleanUpFarmModel(BaseSettingsModel): + _isGroup = True + enabled: bool = Field(True) + + +class PublishPuginsModel(BaseSettingsModel): + CollectAnatomyInstanceData: CollectAnatomyInstanceDataModel = Field( + default_factory=CollectAnatomyInstanceDataModel, + title="Collect Anatomy Instance Data" + ) + CollectAudio: CollectAudioModel = Field( + default_factory=CollectAudioModel, + title="Collect Audio" + ) + CollectSceneVersion: CollectSceneVersionModel = Field( + default_factory=CollectSceneVersionModel, + title="Collect Version from Workfile" + ) + collect_comment_per_instance: CollectCommentPIModel = Field( + default_factory=CollectCommentPIModel, + title="Collect comment per instance", + ) + CollectFramesFixDef: CollectFramesFixDefModel = Field( + default_factory=CollectFramesFixDefModel, + title="Collect Frames to Fix", + ) + ValidateEditorialAssetName: ValidateBaseModel = Field( + default_factory=ValidateBaseModel, + title="Validate Editorial Asset Name" + ) + ValidateVersion: ValidateBaseModel = Field( + default_factory=ValidateBaseModel, + title="Validate Version" + ) + ValidateIntent: ValidateIntentModel = Field( + default_factory=ValidateIntentModel, + title="Validate Intent" + ) + ExtractThumbnail: ExtractThumbnailModel = Field( + default_factory=ExtractThumbnailModel, + title="Extract Thumbnail" + ) + ExtractOIIOTranscode: ExtractOIIOTranscodeModel = Field( + default_factory=ExtractOIIOTranscodeModel, + title="Extract OIIO Transcode" + ) + ExtractReview: ExtractReviewModel = Field( + default_factory=ExtractReviewModel, + title="Extract Review" + ) + ExtractBurnin: ExtractBurninModel = Field( + default_factory=ExtractBurninModel, + title="Extract Burnin" + ) + PreIntegrateThumbnails: PreIntegrateThumbnailsModel = Field( + default_factory=PreIntegrateThumbnailsModel, + title="Override Integrate Thumbnail Representations" + ) + IntegrateProductGroup: IntegrateProductGroupModel = Field( + default_factory=IntegrateProductGroupModel, + title="Integrate Product Group" + ) + IntegrateHeroVersion: IntegrateHeroVersionModel = Field( + default_factory=IntegrateHeroVersionModel, + title="Integrate Hero Version" + ) + CleanUp: CleanUpModel = Field( + default_factory=CleanUpModel, + title="Clean Up" + ) + CleanUpFarm: CleanUpFarmModel = Field( + default_factory=CleanUpFarmModel, + title="Clean Up Farm" + ) + + +DEFAULT_PUBLISH_VALUES = { + "CollectAnatomyInstanceData": { + "follow_workfile_version": False + }, + "CollectAudio": { + "enabled": False, + "audio_product_name": "audioMain" + }, + "CollectSceneVersion": { + "hosts": [ + "aftereffects", + "blender", + "celaction", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint" + ], + "skip_hosts_headless_publish": [] + }, + "collect_comment_per_instance": { + "enabled": False, + "families": [] + }, + "CollectFramesFixDef": { + "enabled": True, + "rewrite_version_enable": True + }, + "ValidateEditorialAssetName": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateVersion": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateIntent": { + "enabled": False, + "profiles": [] + }, + "ExtractThumbnail": { + "enabled": True, + "ffmpeg_args": { + "input": [ + "-apply_trc gamma22" + ], + "output": [] + } + }, + "ExtractOIIOTranscode": { + "enabled": True, + "profiles": [] + }, + "ExtractReview": { + "enabled": True, + "profiles": [ + { + "product_types": [], + "hosts": [], + "outputs": [ + { + "name": "png", + "ext": "png", + "tags": [ + "ftrackreview", + "kitsureview" + ], + "burnins": [], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [], + "output": [] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ], + "product_names": [], + "custom_tags": [], + "single_frame_filter": "single_frame" + }, + "overscan_crop": "", + "overscan_color": [0, 0, 0, 1.0], + "width": 1920, + "height": 1080, + "scale_pixel_aspect": True, + "bg_color": [0, 0, 0, 0.0], + "letter_box": { + "enabled": False, + "ratio": 0.0, + "fill_color": [0, 0, 0, 1.0], + "line_thickness": 0, + "line_color": [255, 0, 0, 1.0] + } + }, + { + "name": "h264", + "ext": "mp4", + "tags": [ + "burnin", + "ftrackreview", + "kitsureview" + ], + "burnins": [], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [ + "-apply_trc gamma22" + ], + "output": [ + "-pix_fmt yuv420p", + "-crf 18", + "-intra" + ] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ], + "product_names": [], + "custom_tags": [], + "single_frame_filter": "multi_frame" + }, + "overscan_crop": "", + "overscan_color": [0, 0, 0, 1.0], + "width": 0, + "height": 0, + "scale_pixel_aspect": True, + "bg_color": [0, 0, 0, 0.0], + "letter_box": { + "enabled": False, + "ratio": 0.0, + "fill_color": [0, 0, 0, 1.0], + "line_thickness": 0, + "line_color": [255, 0, 0, 1.0] + } + } + ] + } + ] + }, + "ExtractBurnin": { + "enabled": True, + "options": { + "font_size": 42, + "font_color": [255, 255, 255, 1.0], + "bg_color": [0, 0, 0, 0.5], + "x_offset": 5, + "y_offset": 5, + "bg_padding": 5, + "font_filepath": { + "windows": "", + "darwin": "", + "linux": "" + } + }, + "profiles": [ + { + "product_types": [], + "hosts": [], + "task_types": [], + "task_names": [], + "product_names": [], + "burnins": [ + { + "name": "burnin", + "TOP_LEFT": "{yy}-{mm}-{dd}", + "TOP_CENTERED": "", + "TOP_RIGHT": "{anatomy[version]}", + "BOTTOM_LEFT": "{username}", + "BOTTOM_CENTERED": "{folder[name]}", + "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}", + "filter": { + "families": [], + "tags": [] + } + }, + ] + }, + { + "product_types": ["review"], + "hosts": [ + "maya", + "houdini", + "max" + ], + "task_types": [], + "task_names": [], + "product_names": [], + "burnins": [ + { + "name": "focal_length_burnin", + "TOP_LEFT": "{yy}-{mm}-{dd}", + "TOP_CENTERED": "{focalLength:.2f} mm", + "TOP_RIGHT": "{anatomy[version]}", + "BOTTOM_LEFT": "{username}", + "BOTTOM_CENTERED": "{folder[name]}", + "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}", + "filter": { + "families": [], + "tags": [] + } + } + ] + } + ] + }, + "PreIntegrateThumbnails": { + "enabled": True, + "integrate_profiles": [] + }, + "IntegrateProductGroup": { + "product_grouping_profiles": [ + { + "product_types": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "" + } + ] + }, + "IntegrateHeroVersion": { + "enabled": True, + "optional": True, + "active": True, + "families": [ + "model", + "rig", + "look", + "pointcache", + "animation", + "setdress", + "layout", + "mayaScene", + "simpleUnrealTexture" + ], + "template_name_profiles": [ + { + "product_types": [ + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "simpleUnrealTextureHero" + } + ] + }, + "CleanUp": { + "paterns": [], + "remove_temp_renders": False + }, + "CleanUpFarm": { + "enabled": False + } +} diff --git a/server_addon/core/server/settings/tools.py b/server_addon/core/server/settings/tools.py new file mode 100644 index 0000000000..7befc795e4 --- /dev/null +++ b/server_addon/core/server/settings/tools.py @@ -0,0 +1,506 @@ +from pydantic import Field, validator +from ayon_server.settings import ( + BaseSettingsModel, + normalize_name, + ensure_unique_names, + task_types_enum, +) + + +class ProductTypeSmartSelectModel(BaseSettingsModel): + _layout = "expanded" + name: str = Field("", title="Product type") + task_names: list[str] = Field(default_factory=list, title="Task names") + + @validator("name") + def normalize_value(cls, value): + return normalize_name(value) + + +class ProductNameProfile(BaseSettingsModel): + _layout = "expanded" + product_types: list[str] = Field( + default_factory=list, title="Product types" + ) + hosts: list[str] = Field(default_factory=list, title="Hosts") + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + tasks: list[str] = Field(default_factory=list, title="Task names") + template: str = Field("", title="Template") + + +class CreatorToolModel(BaseSettingsModel): + # TODO this was dynamic dictionary '{name: task_names}' + product_types_smart_select: list[ProductTypeSmartSelectModel] = Field( + default_factory=list, + title="Create Smart Select" + ) + product_name_profiles: list[ProductNameProfile] = Field( + default_factory=list, + title="Product name profiles" + ) + + @validator("product_types_smart_select") + def validate_unique_name(cls, value): + ensure_unique_names(value) + return value + + +class WorkfileTemplateProfile(BaseSettingsModel): + _layout = "expanded" + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + # TODO this should use hosts enum + hosts: list[str] = Field(default_factory=list, title="Hosts") + # TODO this was using project anatomy template name + workfile_template: str = Field("", title="Workfile template") + + +class LastWorkfileOnStartupProfile(BaseSettingsModel): + _layout = "expanded" + # TODO this should use hosts enum + hosts: list[str] = Field(default_factory=list, title="Hosts") + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + tasks: list[str] = Field(default_factory=list, title="Task names") + enabled: bool = Field(True, title="Enabled") + use_last_published_workfile: bool = Field( + True, title="Use last published workfile" + ) + + +class WorkfilesToolOnStartupProfile(BaseSettingsModel): + _layout = "expanded" + # TODO this should use hosts enum + hosts: list[str] = Field(default_factory=list, title="Hosts") + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + tasks: list[str] = Field(default_factory=list, title="Task names") + enabled: bool = Field(True, title="Enabled") + + +class ExtraWorkFoldersProfile(BaseSettingsModel): + _layout = "expanded" + # TODO this should use hosts enum + hosts: list[str] = Field(default_factory=list, title="Hosts") + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field(default_factory=list, title="Task names") + folders: list[str] = Field(default_factory=list, title="Folders") + + +class WorkfilesLockProfile(BaseSettingsModel): + _layout = "expanded" + # TODO this should use hosts enum + host_names: list[str] = Field(default_factory=list, title="Hosts") + enabled: bool = Field(True, title="Enabled") + + +class WorkfilesToolModel(BaseSettingsModel): + workfile_template_profiles: list[WorkfileTemplateProfile] = Field( + default_factory=list, + title="Workfile template profiles" + ) + last_workfile_on_startup: list[LastWorkfileOnStartupProfile] = Field( + default_factory=list, + title="Open last workfile on launch" + ) + open_workfile_tool_on_startup: list[WorkfilesToolOnStartupProfile] = Field( + default_factory=list, + title="Open workfile tool on launch" + ) + extra_folders: list[ExtraWorkFoldersProfile] = Field( + default_factory=list, + title="Extra work folders" + ) + workfile_lock_profiles: list[WorkfilesLockProfile] = Field( + default_factory=list, + title="Workfile lock profiles" + ) + + +def _product_types_enum(): + return [ + "action", + "animation", + "assembly", + "audio", + "backgroundComp", + "backgroundLayout", + "camera", + "editorial", + "gizmo", + "image", + "layout", + "look", + "matchmove", + "mayaScene", + "model", + "nukenodes", + "plate", + "pointcache", + "prerender", + "redshiftproxy", + "reference", + "render", + "review", + "rig", + "setdress", + "take", + "usdShade", + "vdbcache", + "vrayproxy", + "workfile", + "xgen", + "yetiRig", + "yeticache" + ] + + +class LoaderProductTypeFilterProfile(BaseSettingsModel): + _layout = "expanded" + # TODO this should use hosts enum + hosts: list[str] = Field(default_factory=list, title="Hosts") + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + is_include: bool = Field(True, title="Exclude / Include") + filter_product_types: list[str] = Field( + default_factory=list, + enum_resolver=_product_types_enum + ) + + +class LoaderToolModel(BaseSettingsModel): + product_type_filter_profiles: list[LoaderProductTypeFilterProfile] = Field( + default_factory=list, + title="Product type filtering" + ) + + +class PublishTemplateNameProfile(BaseSettingsModel): + _layout = "expanded" + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + # TODO this should use hosts enum + hosts: list[str] = Field(default_factory=list, title="Hosts") + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field(default_factory=list, title="Task names") + template_name: str = Field("", title="Template name") + + +class CustomStagingDirProfileModel(BaseSettingsModel): + active: bool = Field(True, title="Is active") + hosts: list[str] = Field(default_factory=list, title="Host names") + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field( + default_factory=list, title="Task names" + ) + product_types: list[str] = Field( + default_factory=list, title="Product types" + ) + product_names: list[str] = Field( + default_factory=list, title="Product names" + ) + custom_staging_dir_persistent: bool = Field( + False, title="Custom Staging Folder Persistent" + ) + template_name: str = Field("", title="Template Name") + + +class PublishToolModel(BaseSettingsModel): + template_name_profiles: list[PublishTemplateNameProfile] = Field( + default_factory=list, + title="Template name profiles" + ) + hero_template_name_profiles: list[PublishTemplateNameProfile] = Field( + default_factory=list, + title="Hero template name profiles" + ) + custom_staging_dir_profiles: list[CustomStagingDirProfileModel] = Field( + default_factory=list, + title="Custom Staging Dir Profiles" + ) + + +class GlobalToolsModel(BaseSettingsModel): + creator: CreatorToolModel = Field( + default_factory=CreatorToolModel, + title="Creator" + ) + Workfiles: WorkfilesToolModel = Field( + default_factory=WorkfilesToolModel, + title="Workfiles" + ) + loader: LoaderToolModel = Field( + default_factory=LoaderToolModel, + title="Loader" + ) + publish: PublishToolModel = Field( + default_factory=PublishToolModel, + title="Publish" + ) + + +DEFAULT_TOOLS_VALUES = { + "creator": { + "product_types_smart_select": [ + { + "name": "Render", + "task_names": [ + "light", + "render" + ] + }, + { + "name": "Model", + "task_names": [ + "model" + ] + }, + { + "name": "Layout", + "task_names": [ + "layout" + ] + }, + { + "name": "Look", + "task_names": [ + "look" + ] + }, + { + "name": "Rig", + "task_names": [ + "rigging", + "rig" + ] + } + ], + "product_name_profiles": [ + { + "product_types": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "{product[type]}{variant}" + }, + { + "product_types": [ + "workfile" + ], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "{product[type]}{Task[name]}" + }, + { + "product_types": [ + "render" + ], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "{product[type]}{Task[name]}{Variant}" + }, + { + "product_types": [ + "renderLayer", + "renderPass" + ], + "hosts": [ + "tvpaint" + ], + "task_types": [], + "tasks": [], + "template": "{product[type]}{Task[name]}_{Renderlayer}_{Renderpass}" + }, + { + "product_types": [ + "review", + "workfile" + ], + "hosts": [ + "aftereffects", + "tvpaint" + ], + "task_types": [], + "tasks": [], + "template": "{product[type]}{Task[name]}" + }, + { + "product_types": ["render"], + "hosts": [ + "aftereffects" + ], + "task_types": [], + "tasks": [], + "template": "{product[type]}{Task[name]}{Composition}{Variant}" + }, + { + "product_types": [ + "staticMesh" + ], + "hosts": [ + "maya" + ], + "task_types": [], + "tasks": [], + "template": "S_{folder[name]}{variant}" + }, + { + "product_types": [ + "skeletalMesh" + ], + "hosts": [ + "maya" + ], + "task_types": [], + "tasks": [], + "template": "SK_{folder[name]}{variant}" + } + ] + }, + "Workfiles": { + "workfile_template_profiles": [ + { + "task_types": [], + "hosts": [], + "workfile_template": "work" + }, + { + "task_types": [], + "hosts": [ + "unreal" + ], + "workfile_template": "work_unreal" + } + ], + "last_workfile_on_startup": [ + { + "hosts": [], + "task_types": [], + "tasks": [], + "enabled": True, + "use_last_published_workfile": False + } + ], + "open_workfile_tool_on_startup": [ + { + "hosts": [], + "task_types": [], + "tasks": [], + "enabled": False + } + ], + "extra_folders": [], + "workfile_lock_profiles": [] + }, + "loader": { + "product_type_filter_profiles": [ + { + "hosts": [], + "task_types": [], + "is_include": True, + "filter_product_types": [] + } + ] + }, + "publish": { + "template_name_profiles": [ + { + "product_types": [], + "hosts": [], + "task_types": [], + "task_names": [], + "template_name": "publish" + }, + { + "product_types": [ + "review", + "render", + "prerender" + ], + "hosts": [], + "task_types": [], + "task_names": [], + "template_name": "publish_render" + }, + { + "product_types": [ + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "publish_simpleUnrealTexture" + }, + { + "product_types": [ + "staticMesh", + "skeletalMesh" + ], + "hosts": [ + "maya" + ], + "task_types": [], + "task_names": [], + "template_name": "publish_maya2unreal" + }, + { + "product_types": [ + "online" + ], + "hosts": [ + "traypublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "publish_online" + } + ], + "hero_template_name_profiles": [ + { + "product_types": [ + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "hero_simpleUnrealTextureHero" + } + ] + } +} diff --git a/server_addon/core/server/version.py b/server_addon/core/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/core/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/create_ayon_addon.py b/server_addon/create_ayon_addon.py deleted file mode 100644 index 657f416441..0000000000 --- a/server_addon/create_ayon_addon.py +++ /dev/null @@ -1,140 +0,0 @@ -import os -import re -import shutil -import zipfile -import collections -from pathlib import Path -from typing import Any, Optional, Iterable - -# Patterns of directories to be skipped for server part of addon -IGNORE_DIR_PATTERNS: list[re.Pattern] = [ - re.compile(pattern) - for pattern in { - # Skip directories starting with '.' - r"^\.", - # Skip any pycache folders - "^__pycache__$" - } -] - -# Patterns of files to be skipped for server part of addon -IGNORE_FILE_PATTERNS: list[re.Pattern] = [ - re.compile(pattern) - for pattern in { - # Skip files starting with '.' - # NOTE this could be an issue in some cases - r"^\.", - # Skip '.pyc' files - r"\.pyc$" - } -] - - -def _value_match_regexes(value: str, regexes: Iterable[re.Pattern]) -> bool: - return any( - regex.search(value) - for regex in regexes - ) - - -def find_files_in_subdir( - src_path: str, - ignore_file_patterns: Optional[list[re.Pattern]] = None, - ignore_dir_patterns: Optional[list[re.Pattern]] = None -): - """Find all files to copy in subdirectories of given path. - - All files that match any of the patterns in 'ignore_file_patterns' will - be skipped and any directories that match any of the patterns in - 'ignore_dir_patterns' will be skipped with all subfiles. - - Args: - src_path (str): Path to directory to search in. - ignore_file_patterns (Optional[list[re.Pattern]]): List of regexes - to match files to ignore. - ignore_dir_patterns (Optional[list[re.Pattern]]): List of regexes - to match directories to ignore. - - Returns: - list[tuple[str, str]]: List of tuples with path to file and parent - directories relative to 'src_path'. - """ - - if ignore_file_patterns is None: - ignore_file_patterns = IGNORE_FILE_PATTERNS - - if ignore_dir_patterns is None: - ignore_dir_patterns = IGNORE_DIR_PATTERNS - output: list[tuple[str, str]] = [] - - hierarchy_queue = collections.deque() - hierarchy_queue.append((src_path, [])) - while hierarchy_queue: - item: tuple[str, str] = hierarchy_queue.popleft() - dirpath, parents = item - for name in os.listdir(dirpath): - path = os.path.join(dirpath, name) - if os.path.isfile(path): - if not _value_match_regexes(name, ignore_file_patterns): - items = list(parents) - items.append(name) - output.append((path, os.path.sep.join(items))) - continue - - if not _value_match_regexes(name, ignore_dir_patterns): - items = list(parents) - items.append(name) - hierarchy_queue.append((path, items)) - - return output - - -def main(): - openpype_addon_dir = Path(os.path.dirname(os.path.abspath(__file__))) - server_dir = openpype_addon_dir / "server" - package_root = openpype_addon_dir / "package" - pyproject_path = openpype_addon_dir / "client" / "pyproject.toml" - - root_dir = openpype_addon_dir.parent - openpype_dir = root_dir / "openpype" - version_path = openpype_dir / "version.py" - - # Read version - version_content: dict[str, Any] = {} - with open(str(version_path), "r") as stream: - exec(stream.read(), version_content) - addon_version: str = version_content["__version__"] - - output_dir = package_root / "openpype" / addon_version - private_dir = output_dir / "private" - - # Make sure package dir is empty - if package_root.exists(): - shutil.rmtree(str(package_root)) - # Make sure output dir is created - output_dir.mkdir(parents=True) - - # Copy version - shutil.copy(str(version_path), str(output_dir)) - for subitem in server_dir.iterdir(): - shutil.copy(str(subitem), str(output_dir / subitem.name)) - - # Make sure private dir exists - private_dir.mkdir(parents=True) - - # Copy pyproject.toml - shutil.copy( - str(pyproject_path), - (private_dir / pyproject_path.name) - ) - - # Zip client - zip_filepath = private_dir / "client.zip" - with zipfile.ZipFile(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf: - # Add client code content to zip - for path, sub_path in find_files_in_subdir(str(openpype_dir)): - zipf.write(path, f"{openpype_dir.name}/{sub_path}") - - -if __name__ == "__main__": - main() diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py new file mode 100644 index 0000000000..3b566cec63 --- /dev/null +++ b/server_addon/create_ayon_addons.py @@ -0,0 +1,279 @@ +import os +import sys +import re +import json +import shutil +import zipfile +import platform +import collections +from pathlib import Path +from typing import Any, Optional, Iterable + +# Patterns of directories to be skipped for server part of addon +IGNORE_DIR_PATTERNS: list[re.Pattern] = [ + re.compile(pattern) + for pattern in { + # Skip directories starting with '.' + r"^\.", + # Skip any pycache folders + "^__pycache__$" + } +] + +# Patterns of files to be skipped for server part of addon +IGNORE_FILE_PATTERNS: list[re.Pattern] = [ + re.compile(pattern) + for pattern in { + # Skip files starting with '.' + # NOTE this could be an issue in some cases + r"^\.", + # Skip '.pyc' files + r"\.pyc$" + } +] + + +class ZipFileLongPaths(zipfile.ZipFile): + """Allows longer paths in zip files. + + Regular DOS paths are limited to MAX_PATH (260) characters, including + the string's terminating NUL character. + That limit can be exceeded by using an extended-length path that + starts with the '\\?\' prefix. + """ + _is_windows = platform.system().lower() == "windows" + + def _extract_member(self, member, tpath, pwd): + if self._is_windows: + tpath = os.path.abspath(tpath) + if tpath.startswith("\\\\"): + tpath = "\\\\?\\UNC\\" + tpath[2:] + else: + tpath = "\\\\?\\" + tpath + + return super(ZipFileLongPaths, self)._extract_member( + member, tpath, pwd + ) + + +def _value_match_regexes(value: str, regexes: Iterable[re.Pattern]) -> bool: + return any( + regex.search(value) + for regex in regexes + ) + + +def find_files_in_subdir( + src_path: str, + ignore_file_patterns: Optional[list[re.Pattern]] = None, + ignore_dir_patterns: Optional[list[re.Pattern]] = None +): + """Find all files to copy in subdirectories of given path. + + All files that match any of the patterns in 'ignore_file_patterns' will + be skipped and any directories that match any of the patterns in + 'ignore_dir_patterns' will be skipped with all subfiles. + + Args: + src_path (str): Path to directory to search in. + ignore_file_patterns (Optional[list[re.Pattern]]): List of regexes + to match files to ignore. + ignore_dir_patterns (Optional[list[re.Pattern]]): List of regexes + to match directories to ignore. + + Returns: + list[tuple[str, str]]: List of tuples with path to file and parent + directories relative to 'src_path'. + """ + + if ignore_file_patterns is None: + ignore_file_patterns = IGNORE_FILE_PATTERNS + + if ignore_dir_patterns is None: + ignore_dir_patterns = IGNORE_DIR_PATTERNS + output: list[tuple[str, str]] = [] + + hierarchy_queue = collections.deque() + hierarchy_queue.append((src_path, [])) + while hierarchy_queue: + item: tuple[str, str] = hierarchy_queue.popleft() + dirpath, parents = item + for name in os.listdir(dirpath): + path = os.path.join(dirpath, name) + if os.path.isfile(path): + if not _value_match_regexes(name, ignore_file_patterns): + items = list(parents) + items.append(name) + output.append((path, os.path.sep.join(items))) + continue + + if not _value_match_regexes(name, ignore_dir_patterns): + items = list(parents) + items.append(name) + hierarchy_queue.append((path, items)) + + return output + + +def read_addon_version(version_path: Path) -> str: + # Read version + version_content: dict[str, Any] = {} + with open(str(version_path), "r") as stream: + exec(stream.read(), version_content) + return version_content["__version__"] + + +def get_addon_version(addon_dir: Path) -> str: + return read_addon_version(addon_dir / "server" / "version.py") + + +def create_addon_zip( + output_dir: Path, + addon_name: str, + addon_version: str, + keep_source: bool +): + zip_filepath = output_dir / f"{addon_name}.zip" + addon_output_dir = output_dir / addon_name / addon_version + with ZipFileLongPaths(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf: + zipf.writestr( + "manifest.json", + json.dumps({ + "addon_name": addon_name, + "addon_version": addon_version + }) + ) + # Add client code content to zip + src_root = os.path.normpath(str(addon_output_dir.absolute())) + src_root_offset = len(src_root) + 1 + for root, _, filenames in os.walk(str(addon_output_dir)): + rel_root = "" + if root != src_root: + rel_root = root[src_root_offset:] + + for filename in filenames: + src_path = os.path.join(root, filename) + if rel_root: + dst_path = os.path.join("addon", rel_root, filename) + else: + dst_path = os.path.join("addon", filename) + zipf.write(src_path, dst_path) + + if not keep_source: + shutil.rmtree(str(output_dir / addon_name)) + + +def create_openpype_package( + addon_dir: Path, + output_dir: Path, + root_dir: Path, + create_zip: bool, + keep_source: bool +): + server_dir = addon_dir / "server" + pyproject_path = addon_dir / "client" / "pyproject.toml" + + openpype_dir = root_dir / "openpype" + version_path = openpype_dir / "version.py" + addon_version = read_addon_version(version_path) + + addon_output_dir = output_dir / "openpype" / addon_version + private_dir = addon_output_dir / "private" + # Make sure dir exists + addon_output_dir.mkdir(parents=True) + private_dir.mkdir(parents=True) + + # Copy version + shutil.copy(str(version_path), str(addon_output_dir)) + for subitem in server_dir.iterdir(): + shutil.copy(str(subitem), str(addon_output_dir / subitem.name)) + + # Copy pyproject.toml + shutil.copy( + str(pyproject_path), + (private_dir / pyproject_path.name) + ) + + # Zip client + zip_filepath = private_dir / "client.zip" + with ZipFileLongPaths(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf: + # Add client code content to zip + for path, sub_path in find_files_in_subdir(str(openpype_dir)): + zipf.write(path, f"{openpype_dir.name}/{sub_path}") + + if create_zip: + create_addon_zip(output_dir, "openpype", addon_version, keep_source) + + +def create_addon_package( + addon_dir: Path, + output_dir: Path, + create_zip: bool, + keep_source: bool +): + server_dir = addon_dir / "server" + addon_version = get_addon_version(addon_dir) + + addon_output_dir = output_dir / addon_dir.name / addon_version + if addon_output_dir.exists(): + shutil.rmtree(str(addon_output_dir)) + addon_output_dir.mkdir(parents=True) + + # Copy server content + src_root = os.path.normpath(str(server_dir.absolute())) + src_root_offset = len(src_root) + 1 + for root, _, filenames in os.walk(str(server_dir)): + dst_root = addon_output_dir + if root != src_root: + rel_root = root[src_root_offset:] + dst_root = dst_root / rel_root + + dst_root.mkdir(parents=True, exist_ok=True) + for filename in filenames: + src_path = os.path.join(root, filename) + shutil.copy(src_path, str(dst_root)) + + if create_zip: + create_addon_zip( + output_dir, addon_dir.name, addon_version, keep_source + ) + + +def main(create_zip=True, keep_source=False): + current_dir = Path(os.path.dirname(os.path.abspath(__file__))) + root_dir = current_dir.parent + output_dir = current_dir / "packages" + print("Package creation started...") + + # Make sure package dir is empty + if output_dir.exists(): + shutil.rmtree(str(output_dir)) + # Make sure output dir is created + output_dir.mkdir(parents=True) + + for addon_dir in current_dir.iterdir(): + if not addon_dir.is_dir(): + continue + + server_dir = addon_dir / "server" + if not server_dir.exists(): + continue + + if addon_dir.name == "openpype": + create_openpype_package( + addon_dir, output_dir, root_dir, create_zip, keep_source + ) + + else: + create_addon_package( + addon_dir, output_dir, create_zip, keep_source + ) + + print(f"- package '{addon_dir.name}' created") + print(f"Package creation finished. Output directory: {output_dir}") + + +if __name__ == "__main__": + create_zip = "--skip-zip" not in sys.argv + keep_sources = "--keep-sources" in sys.argv + main(create_zip, keep_sources) diff --git a/server_addon/deadline/server/__init__.py b/server_addon/deadline/server/__init__.py new file mode 100644 index 0000000000..36d04189a9 --- /dev/null +++ b/server_addon/deadline/server/__init__.py @@ -0,0 +1,17 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import DeadlineSettings, DEFAULT_VALUES + + +class Deadline(BaseServerAddon): + name = "deadline" + title = "Deadline" + version = __version__ + settings_model: Type[DeadlineSettings] = DeadlineSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/deadline/server/settings/__init__.py b/server_addon/deadline/server/settings/__init__.py new file mode 100644 index 0000000000..0307862afa --- /dev/null +++ b/server_addon/deadline/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + DeadlineSettings, + DEFAULT_VALUES, +) + + +__all__ = ( + "DeadlineSettings", + "DEFAULT_VALUES", +) diff --git a/server_addon/deadline/server/settings/main.py b/server_addon/deadline/server/settings/main.py new file mode 100644 index 0000000000..e60df2eda3 --- /dev/null +++ b/server_addon/deadline/server/settings/main.py @@ -0,0 +1,48 @@ +from pydantic import Field, validator + +from ayon_server.settings import BaseSettingsModel, ensure_unique_names + +from .publish_plugins import ( + PublishPluginsModel, + DEFAULT_DEADLINE_PLUGINS_SETTINGS +) + + +class ServerListSubmodel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: str = Field(title="Value") + + +class DeadlineSettings(BaseSettingsModel): + deadline_urls: list[ServerListSubmodel] = Field( + default_factory=list, + title="System Deadline Webservice URLs", + ) + + deadline_servers: list[str] = Field( + title="Project deadline servers", + section="---") + + publish: PublishPluginsModel = Field( + default_factory=PublishPluginsModel, + title="Publish Plugins", + ) + + @validator("deadline_urls") + def validate_unique_names(cls, value): + ensure_unique_names(value) + return value + + +DEFAULT_VALUES = { + "deadline_urls": [ + { + "name": "default", + "value": "http://127.0.0.1:8082" + } + ], + # TODO: this needs to be dynamic from "deadline_urls" + "deadline_servers": [], + "publish": DEFAULT_DEADLINE_PLUGINS_SETTINGS +} diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py new file mode 100644 index 0000000000..8d1b667345 --- /dev/null +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -0,0 +1,435 @@ +from pydantic import Field, validator + +from ayon_server.settings import BaseSettingsModel, ensure_unique_names + + +class CollectDefaultDeadlineServerModel(BaseSettingsModel): + """Settings for event handlers running in ftrack service.""" + + pass_mongo_url: bool = Field(title="Pass Mongo url to job") + + +class CollectDeadlinePoolsModel(BaseSettingsModel): + """Settings Deadline default pools.""" + + primary_pool: str = Field(title="Primary Pool") + + secondary_pool: str = Field(title="Secondary Pool") + + +class ValidateExpectedFilesModel(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + active: bool = Field(True, title="Active") + allow_user_override: bool = Field( + True, title="Allow user change frame range" + ) + families: list[str] = Field( + default_factory=list, title="Trigger on families" + ) + targets: list[str] = Field( + default_factory=list, title="Trigger for plugins" + ) + + +def tile_assembler_enum(): + """Return a list of value/label dicts for the enumerator. + + Returning a list of dicts is used to allow for a custom label to be + displayed in the UI. + """ + return [ + { + "value": "DraftTileAssembler", + "label": "Draft Tile Assembler" + }, + { + "value": "OpenPypeTileAssembler", + "label": "Open Image IO" + } + ] + + +class ScenePatchesSubmodel(BaseSettingsModel): + _layout = "expanded" + name: str = Field(title="Patch name") + regex: str = Field(title="Patch regex") + line: str = Field(title="Patch line") + + +class MayaSubmitDeadlineModel(BaseSettingsModel): + """Maya deadline submitter settings.""" + + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + use_published: bool = Field(title="Use Published scene") + import_reference: bool = Field(title="Use Scene with Imported Reference") + asset_dependencies: bool = Field(title="Use Asset dependencies") + priority: int = Field(title="Priority") + tile_priority: int = Field(title="Tile Priority") + group: str = Field(title="Group") + limit: list[str] = Field( + default_factory=list, + title="Limit Groups" + ) + tile_assembler_plugin: str = Field( + title="Tile Assembler Plugin", + enum_resolver=tile_assembler_enum, + ) + jobInfo: str = Field( + title="Additional JobInfo data", + widget="textarea", + ) + pluginInfo: str = Field( + title="Additional PluginInfo data", + widget="textarea", + ) + + scene_patches: list[ScenePatchesSubmodel] = Field( + default_factory=list, + title="Scene patches", + ) + strict_error_checking: bool = Field( + title="Disable Strict Error Check profiles" + ) + + @validator("limit", "scene_patches") + def validate_unique_names(cls, value): + ensure_unique_names(value) + return value + + +class MaxSubmitDeadlineModel(BaseSettingsModel): + enabled: bool = Field(True) + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + use_published: bool = Field(title="Use Published scene") + priority: int = Field(title="Priority") + chunk_size: int = Field(title="Frame per Task") + group: str = Field("", title="Group Name") + + +class EnvSearchReplaceSubmodel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: str = Field(title="Value") + + +class LimitGroupsSubmodel(BaseSettingsModel): + _layout = "expanded" + name: str = Field(title="Name") + value: list[str] = Field( + default_factory=list, + title="Limit Groups" + ) + + +class FusionSubmitDeadlineModel(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + optional: bool = Field(False, title="Optional") + active: bool = Field(True, title="Active") + priority: int = Field(50, title="Priority") + chunk_size: int = Field(10, title="Frame per Task") + concurrent_tasks: int = Field(1, title="Number of concurrent tasks") + group: str = Field("", title="Group Name") + + +class NukeSubmitDeadlineModel(BaseSettingsModel): + """Nuke deadline submitter settings.""" + + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + priority: int = Field(title="Priority") + chunk_size: int = Field(title="Chunk Size") + concurrent_tasks: int = Field(title="Number of concurrent tasks") + group: str = Field(title="Group") + department: str = Field(title="Department") + use_gpu: bool = Field(title="Use GPU") + + env_allowed_keys: list[str] = Field( + default_factory=list, + title="Allowed environment keys" + ) + + env_search_replace_values: list[EnvSearchReplaceSubmodel] = Field( + default_factory=list, + title="Search & replace in environment values", + ) + + limit_groups: list[LimitGroupsSubmodel] = Field( + default_factory=list, + title="Limit Groups", + ) + + @validator("limit_groups", "env_allowed_keys", "env_search_replace_values") + def validate_unique_names(cls, value): + ensure_unique_names(value) + return value + + +class HarmonySubmitDeadlineModel(BaseSettingsModel): + """Harmony deadline submitter settings.""" + + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + use_published: bool = Field(title="Use Published scene") + priority: int = Field(title="Priority") + chunk_size: int = Field(title="Chunk Size") + group: str = Field(title="Group") + department: str = Field(title="Department") + + +class AfterEffectsSubmitDeadlineModel(BaseSettingsModel): + """After Effects deadline submitter settings.""" + + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + use_published: bool = Field(title="Use Published scene") + priority: int = Field(title="Priority") + chunk_size: int = Field(title="Chunk Size") + group: str = Field(title="Group") + department: str = Field(title="Department") + multiprocess: bool = Field(title="Optional") + + +class CelactionSubmitDeadlineModel(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + deadline_department: str = Field("", title="Deadline apartment") + deadline_priority: int = Field(50, title="Deadline priority") + deadline_pool: str = Field("", title="Deadline pool") + deadline_pool_secondary: str = Field("", title="Deadline pool (secondary)") + deadline_group: str = Field("", title="Deadline Group") + deadline_chunk_size: int = Field(10, title="Deadline Chunk size") + deadline_job_delay: str = Field( + "", title="Delay job (timecode dd:hh:mm:ss)" + ) + + +class AOVFilterSubmodel(BaseSettingsModel): + _layout = "expanded" + name: str = Field(title="Host") + value: list[str] = Field( + default_factory=list, + title="AOV regex" + ) + + +class ProcessSubmittedJobOnFarmModel(BaseSettingsModel): + """Process submitted job on farm.""" + + enabled: bool = Field(title="Enabled") + deadline_department: str = Field(title="Department") + deadline_pool: str = Field(title="Pool") + deadline_group: str = Field(title="Group") + deadline_chunk_size: int = Field(title="Chunk Size") + deadline_priority: int = Field(title="Priority") + publishing_script: str = Field(title="Publishing script path") + skip_integration_repre_list: list[str] = Field( + default_factory=list, + title="Skip integration of representation with ext" + ) + aov_filter: list[AOVFilterSubmodel] = Field( + default_factory=list, + title="Reviewable products filter", + ) + + @validator("aov_filter", "skip_integration_repre_list") + def validate_unique_names(cls, value): + ensure_unique_names(value) + return value + + +class PublishPluginsModel(BaseSettingsModel): + CollectDefaultDeadlineServer: CollectDefaultDeadlineServerModel = Field( + default_factory=CollectDefaultDeadlineServerModel, + title="Default Deadline Webservice") + CollectDefaultDeadlineServer: CollectDefaultDeadlineServerModel = Field( + default_factory=CollectDefaultDeadlineServerModel, + title="Default Deadline Webservice") + CollectDeadlinePools: CollectDeadlinePoolsModel = Field( + default_factory=CollectDeadlinePoolsModel, + title="Default Pools") + ValidateExpectedFiles: ValidateExpectedFilesModel = Field( + default_factory=ValidateExpectedFilesModel, + title="Validate Expected Files" + ) + MayaSubmitDeadline: MayaSubmitDeadlineModel = Field( + default_factory=MayaSubmitDeadlineModel, + title="Maya Submit to deadline") + MaxSubmitDeadline: MaxSubmitDeadlineModel = Field( + default_factory=MaxSubmitDeadlineModel, + title="Max Submit to deadline") + FusionSubmitDeadline: FusionSubmitDeadlineModel = Field( + default_factory=FusionSubmitDeadlineModel, + title="Fusion submit to Deadline") + NukeSubmitDeadline: NukeSubmitDeadlineModel = Field( + default_factory=NukeSubmitDeadlineModel, + title="Nuke Submit to deadline") + HarmonySubmitDeadline: HarmonySubmitDeadlineModel = Field( + default_factory=HarmonySubmitDeadlineModel, + title="Harmony Submit to deadline") + AfterEffectsSubmitDeadline: AfterEffectsSubmitDeadlineModel = Field( + default_factory=AfterEffectsSubmitDeadlineModel, + title="After Effects to deadline") + CelactionSubmitDeadline: CelactionSubmitDeadlineModel = Field( + default_factory=CelactionSubmitDeadlineModel, + title="Celaction Submit Deadline" + ) + ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = Field( + default_factory=ProcessSubmittedJobOnFarmModel, + title="Process submitted job on farm.") + + +DEFAULT_DEADLINE_PLUGINS_SETTINGS = { + "CollectDefaultDeadlineServer": { + "pass_mongo_url": True + }, + "CollectDeadlinePools": { + "primary_pool": "", + "secondary_pool": "" + }, + "ValidateExpectedFiles": { + "enabled": True, + "active": True, + "allow_user_override": True, + "families": [ + "render" + ], + "targets": [ + "deadline" + ] + }, + "MayaSubmitDeadline": { + "enabled": True, + "optional": False, + "active": True, + "tile_assembler_plugin": "DraftTileAssembler", + "use_published": True, + "import_reference": False, + "asset_dependencies": True, + "strict_error_checking": True, + "priority": 50, + "tile_priority": 50, + "group": "none", + "limit": [], + # this used to be empty dict + "jobInfo": "", + # this used to be empty dict + "pluginInfo": "", + "scene_patches": [] + }, + "MaxSubmitDeadline": { + "enabled": True, + "optional": False, + "active": True, + "use_published": True, + "priority": 50, + "chunk_size": 10, + "group": "none" + }, + "FusionSubmitDeadline": { + "enabled": True, + "optional": False, + "active": True, + "priority": 50, + "chunk_size": 10, + "concurrent_tasks": 1, + "group": "" + }, + "NukeSubmitDeadline": { + "enabled": True, + "optional": False, + "active": True, + "priority": 50, + "chunk_size": 10, + "concurrent_tasks": 1, + "group": "", + "department": "", + "use_gpu": True, + "env_allowed_keys": [], + "env_search_replace_values": [], + "limit_groups": [] + }, + "HarmonySubmitDeadline": { + "enabled": True, + "optional": False, + "active": True, + "use_published": True, + "priority": 50, + "chunk_size": 10000, + "group": "", + "department": "" + }, + "AfterEffectsSubmitDeadline": { + "enabled": True, + "optional": False, + "active": True, + "use_published": True, + "priority": 50, + "chunk_size": 10000, + "group": "", + "department": "", + "multiprocess": True + }, + "CelactionSubmitDeadline": { + "enabled": True, + "deadline_department": "", + "deadline_priority": 50, + "deadline_pool": "", + "deadline_pool_secondary": "", + "deadline_group": "", + "deadline_chunk_size": 10, + "deadline_job_delay": "00:00:00:00" + }, + "ProcessSubmittedJobOnFarm": { + "enabled": True, + "deadline_department": "", + "deadline_pool": "", + "deadline_group": "", + "deadline_chunk_size": 1, + "deadline_priority": 50, + "publishing_script": "", + "skip_integration_repre_list": [], + "aov_filter": [ + { + "name": "maya", + "value": [ + ".*([Bb]eauty).*" + ] + }, + { + "name": "aftereffects", + "value": [ + ".*" + ] + }, + { + "name": "celaction", + "value": [ + ".*" + ] + }, + { + "name": "harmony", + "value": [ + ".*" + ] + }, + { + "name": "max", + "value": [ + ".*" + ] + }, + { + "name": "fusion", + "value": [ + ".*" + ] + } + ] + } +} diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/deadline/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/flame/server/__init__.py b/server_addon/flame/server/__init__.py new file mode 100644 index 0000000000..7d5eb3960f --- /dev/null +++ b/server_addon/flame/server/__init__.py @@ -0,0 +1,19 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import FlameSettings, DEFAULT_VALUES + + +class FlameAddon(BaseServerAddon): + name = "flame" + title = "Flame" + version = __version__ + settings_model: Type[FlameSettings] = FlameSettings + frontend_scopes = {} + services = {} + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/flame/server/settings/__init__.py b/server_addon/flame/server/settings/__init__.py new file mode 100644 index 0000000000..39b8220d40 --- /dev/null +++ b/server_addon/flame/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + FlameSettings, + DEFAULT_VALUES, +) + + +__all__ = ( + "FlameSettings", + "DEFAULT_VALUES", +) diff --git a/server_addon/flame/server/settings/create_plugins.py b/server_addon/flame/server/settings/create_plugins.py new file mode 100644 index 0000000000..374a7368d2 --- /dev/null +++ b/server_addon/flame/server/settings/create_plugins.py @@ -0,0 +1,120 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class CreateShotClipModel(BaseSettingsModel): + hierarchy: str = Field( + "shot", + title="Shot parent hierarchy", + section="Shot Hierarchy And Rename Settings" + ) + useShotName: bool = Field( + True, + title="Use Shot Name", + ) + clipRename: bool = Field( + False, + title="Rename clips", + ) + clipName: str = Field( + "{sequence}{shot}", + title="Clip name template" + ) + segmentIndex: bool = Field( + True, + title="Accept segment order" + ) + countFrom: int = Field( + 10, + title="Count sequence from" + ) + countSteps: int = Field( + 10, + title="Stepping number" + ) + + folder: str = Field( + "shots", + title="{folder}", + section="Shot Template Keywords" + ) + episode: str = Field( + "ep01", + title="{episode}" + ) + sequence: str = Field( + "a", + title="{sequence}" + ) + track: str = Field( + "{_track_}", + title="{track}" + ) + shot: str = Field( + "####", + title="{shot}" + ) + + vSyncOn: bool = Field( + False, + title="Enable Vertical Sync", + section="Vertical Synchronization Of Attributes" + ) + + workfileFrameStart: int = Field( + 1001, + title="Workfiles Start Frame", + section="Shot Attributes" + ) + handleStart: int = Field( + 10, + title="Handle start (head)" + ) + handleEnd: int = Field( + 10, + title="Handle end (tail)" + ) + includeHandles: bool = Field( + False, + title="Enable handles including" + ) + retimedHandles: bool = Field( + True, + title="Enable retimed handles" + ) + retimedFramerange: bool = Field( + True, + title="Enable retimed shot frameranges" + ) + + +class CreatePuginsModel(BaseSettingsModel): + CreateShotClip: CreateShotClipModel = Field( + default_factory=CreateShotClipModel, + title="Create Shot Clip" + ) + + +DEFAULT_CREATE_SETTINGS = { + "CreateShotClip": { + "hierarchy": "{folder}/{sequence}", + "useShotName": True, + "clipRename": False, + "clipName": "{sequence}{shot}", + "segmentIndex": True, + "countFrom": 10, + "countSteps": 10, + "folder": "shots", + "episode": "ep01", + "sequence": "a", + "track": "{_track_}", + "shot": "####", + "vSyncOn": False, + "workfileFrameStart": 1001, + "handleStart": 5, + "handleEnd": 5, + "includeHandles": False, + "retimedHandles": True, + "retimedFramerange": True + } +} diff --git a/server_addon/flame/server/settings/imageio.py b/server_addon/flame/server/settings/imageio.py new file mode 100644 index 0000000000..ef1e4721d1 --- /dev/null +++ b/server_addon/flame/server/settings/imageio.py @@ -0,0 +1,130 @@ +from pydantic import Field, validator +from ayon_server.settings import BaseSettingsModel, ensure_unique_names + + +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 ImageIORemappingRulesModel(BaseSettingsModel): + host_native_name: str = Field( + title="Application native colorspace name" + ) + ocio_name: str = Field(title="OCIO colorspace name") + + +class ImageIORemappingModel(BaseSettingsModel): + rules: list[ImageIORemappingRulesModel] = Field( + default_factory=list + ) + + +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 ProfileNamesMappingInputsModel(BaseSettingsModel): + _layout = "expanded" + + flameName: str = Field("", title="Flame name") + ocioName: str = Field("", title="OCIO name") + + +class ProfileNamesMappingModel(BaseSettingsModel): + _layout = "expanded" + + inputs: list[ProfileNamesMappingInputsModel] = Field( + default_factory=list, + title="Profile names mapping" + ) + + +class ImageIOProjectModel(BaseSettingsModel): + colourPolicy: str = Field( + "ACES 1.1", + title="Colour Policy (name or path)", + section="Project" + ) + frameDepth: str = Field( + "16-bit fp", + title="Image Depth" + ) + fieldDominance: str = Field( + "PROGRESSIVE", + title="Field Dominance" + ) + + +class FlameImageIOModel(BaseSettingsModel): + _isGroup = True + activate_host_color_management: bool = Field( + True, title="Enable Color Management" + ) + remapping: ImageIORemappingModel = Field( + title="Remapping colorspace names", + default_factory=ImageIORemappingModel + ) + ocio_config: ImageIOConfigModel = Field( + default_factory=ImageIOConfigModel, + title="OCIO config" + ) + file_rules: ImageIOFileRulesModel = Field( + default_factory=ImageIOFileRulesModel, + title="File Rules" + ) + # NOTE 'project' attribute was expanded to this model but that caused + # inconsistency with v3 settings and harder conversion handling + # - it can be moved back but keep in mind that it must be handled in v3 + # conversion script too + project: ImageIOProjectModel = Field( + default_factory=ImageIOProjectModel, + title="Project" + ) + profilesMapping: ProfileNamesMappingModel = Field( + default_factory=ProfileNamesMappingModel, + title="Profile names mapping" + ) + + +DEFAULT_IMAGEIO_SETTINGS = { + "project": { + "colourPolicy": "ACES 1.1", + "frameDepth": "16-bit fp", + "fieldDominance": "PROGRESSIVE" + }, + "profilesMapping": { + "inputs": [ + { + "flameName": "ACEScg", + "ocioName": "ACES - ACEScg" + }, + { + "flameName": "Rec.709 video", + "ocioName": "Output - Rec.709" + } + ] + } +} diff --git a/server_addon/flame/server/settings/loader_plugins.py b/server_addon/flame/server/settings/loader_plugins.py new file mode 100644 index 0000000000..6c27b926c2 --- /dev/null +++ b/server_addon/flame/server/settings/loader_plugins.py @@ -0,0 +1,99 @@ +from ayon_server.settings import Field, BaseSettingsModel + + +class LoadClipModel(BaseSettingsModel): + enabled: bool = Field(True) + + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + reel_group_name: str = Field( + "OpenPype_Reels", + title="Reel group name" + ) + reel_name: str = Field( + "Loaded", + title="Reel name" + ) + + clip_name_template: str = Field( + "{folder[name]}_{product[name]}<_{output}>", + title="Clip name template" + ) + layer_rename_template: str = Field("", title="Layer name template") + layer_rename_patterns: list[str] = Field( + default_factory=list, + title="Layer rename patters", + ) + + +class LoadClipBatchModel(BaseSettingsModel): + enabled: bool = Field(True) + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + reel_name: str = Field( + "OP_LoadedReel", + title="Reel name" + ) + clip_name_template: str = Field( + "{batch}_{folder[name]}_{product[name]}<_{output}>", + title="Clip name template" + ) + layer_rename_template: str = Field("", title="Layer name template") + layer_rename_patterns: list[str] = Field( + default_factory=list, + title="Layer rename patters", + ) + + +class LoaderPluginsModel(BaseSettingsModel): + LoadClip: LoadClipModel = Field( + default_factory=LoadClipModel, + title="Load Clip" + ) + LoadClipBatch: LoadClipBatchModel = Field( + default_factory=LoadClipBatchModel, + title="Load as clip to current batch" + ) + + +DEFAULT_LOADER_SETTINGS = { + "LoadClip": { + "enabled": True, + "product_types": [ + "render2d", + "source", + "plate", + "render", + "review" + ], + "reel_group_name": "OpenPype_Reels", + "reel_name": "Loaded", + "clip_name_template": "{folder[name]}_{product[name]}<_{output}>", + "layer_rename_template": "{folder[name]}_{product[name]}<_{output}>", + "layer_rename_patterns": [ + "rgb", + "rgba" + ] + }, + "LoadClipBatch": { + "enabled": True, + "product_types": [ + "render2d", + "source", + "plate", + "render", + "review" + ], + "reel_name": "OP_LoadedReel", + "clip_name_template": "{batch}_{folder[name]}_{product[name]}<_{output}>", + "layer_rename_template": "{folder[name]}_{product[name]}<_{output}>", + "layer_rename_patterns": [ + "rgb", + "rgba" + ] + } +} diff --git a/server_addon/flame/server/settings/main.py b/server_addon/flame/server/settings/main.py new file mode 100644 index 0000000000..f28de6641b --- /dev/null +++ b/server_addon/flame/server/settings/main.py @@ -0,0 +1,33 @@ +from ayon_server.settings import Field, BaseSettingsModel + +from .imageio import FlameImageIOModel, DEFAULT_IMAGEIO_SETTINGS +from .create_plugins import CreatePuginsModel, DEFAULT_CREATE_SETTINGS +from .publish_plugins import PublishPuginsModel, DEFAULT_PUBLISH_SETTINGS +from .loader_plugins import LoaderPluginsModel, DEFAULT_LOADER_SETTINGS + + +class FlameSettings(BaseSettingsModel): + imageio: FlameImageIOModel = Field( + default_factory=FlameImageIOModel, + title="Color Management (ImageIO)" + ) + create: CreatePuginsModel = Field( + default_factory=CreatePuginsModel, + title="Create plugins" + ) + publish: PublishPuginsModel = Field( + default_factory=PublishPuginsModel, + title="Publish plugins" + ) + load: LoaderPluginsModel = Field( + default_factory=LoaderPluginsModel, + title="Loader plugins" + ) + + +DEFAULT_VALUES = { + "imageio": DEFAULT_IMAGEIO_SETTINGS, + "create": DEFAULT_CREATE_SETTINGS, + "publish": DEFAULT_PUBLISH_SETTINGS, + "load": DEFAULT_LOADER_SETTINGS +} diff --git a/server_addon/flame/server/settings/publish_plugins.py b/server_addon/flame/server/settings/publish_plugins.py new file mode 100644 index 0000000000..ea7f109f73 --- /dev/null +++ b/server_addon/flame/server/settings/publish_plugins.py @@ -0,0 +1,190 @@ +from ayon_server.settings import Field, BaseSettingsModel, task_types_enum + + +class XMLPresetAttrsFromCommentsModel(BaseSettingsModel): + _layout = "expanded" + name: str = Field("", title="Attribute name") + type: str = Field( + default_factory=str, + title="Attribute type", + enum_resolver=lambda: ["number", "float", "string"] + ) + + +class AddTasksModel(BaseSettingsModel): + _layout = "expanded" + name: str = Field("", title="Task name") + type: str = Field( + default_factory=str, + title="Task type", + enum_resolver=task_types_enum + ) + create_batch_group: bool = Field( + True, + title="Create batch group" + ) + + +class CollectTimelineInstancesModel(BaseSettingsModel): + _isGroup = True + + xml_preset_attrs_from_comments: list[XMLPresetAttrsFromCommentsModel] = Field( + default_factory=list, + title="XML presets attributes parsable from segment comments" + ) + add_tasks: list[AddTasksModel] = Field( + default_factory=list, + title="Add tasks" + ) + + +class ExportPresetsMappingModel(BaseSettingsModel): + _layout = "expanded" + + name: str = Field( + ..., + title="Name" + ) + active: bool = Field(True, title="Is active") + export_type: str = Field( + "File Sequence", + title="Eport clip type", + enum_resolver=lambda: ["Movie", "File Sequence", "Sequence Publish"] + ) + ext: str = Field("exr", title="Output extension") + xml_preset_file: str = Field( + "OpenEXR (16-bit fp DWAA).xml", + title="XML preset file (with ext)" + ) + colorspace_out: str = Field( + "ACES - ACEScg", + title="Output color (imageio)" + ) + # TODO remove when resolved or v3 is not a thing anymore + # NOTE next 4 attributes were grouped under 'other_parameters' but that + # created inconsistency with v3 settings and harder conversion handling + # - it can be moved back but keep in mind that it must be handled in v3 + # conversion script too + xml_preset_dir: str = Field( + "", + title="XML preset directory" + ) + parsed_comment_attrs: bool = Field( + True, + title="Parsed comment attributes" + ) + representation_add_range: bool = Field( + True, + title="Add range to representation name" + ) + representation_tags: list[str] = Field( + default_factory=list, + title="Representation tags" + ) + load_to_batch_group: bool = Field( + True, + title="Load to batch group reel" + ) + batch_group_loader_name: str = Field( + "LoadClipBatch", + title="Use loader name" + ) + filter_path_regex: str = Field( + ".*", + title="Regex in clip path" + ) + + +class ExtractProductResourcesModel(BaseSettingsModel): + _isGroup = True + + keep_original_representation: bool = Field( + False, + title="Publish clip's original media" + ) + export_presets_mapping: list[ExportPresetsMappingModel] = Field( + default_factory=list, + title="Export presets mapping" + ) + + +class IntegrateBatchGroupModel(BaseSettingsModel): + enabled: bool = Field( + False, + title="Enabled" + ) + + +class PublishPuginsModel(BaseSettingsModel): + CollectTimelineInstances: CollectTimelineInstancesModel = Field( + default_factory=CollectTimelineInstancesModel, + title="Collect Timeline Instances" + ) + + ExtractProductResources: ExtractProductResourcesModel = Field( + default_factory=ExtractProductResourcesModel, + title="Extract Product Resources" + ) + + IntegrateBatchGroup: IntegrateBatchGroupModel = Field( + default_factory=IntegrateBatchGroupModel, + title="IntegrateBatchGroup" + ) + + +DEFAULT_PUBLISH_SETTINGS = { + "CollectTimelineInstances": { + "xml_preset_attrs_from_comments": [ + { + "name": "width", + "type": "number" + }, + { + "name": "height", + "type": "number" + }, + { + "name": "pixelRatio", + "type": "float" + }, + { + "name": "resizeType", + "type": "string" + }, + { + "name": "resizeFilter", + "type": "string" + } + ], + "add_tasks": [ + { + "name": "compositing", + "type": "Compositing", + "create_batch_group": True + } + ] + }, + "ExtractProductResources": { + "keep_original_representation": False, + "export_presets_mapping": [ + { + "name": "exr16fpdwaa", + "active": True, + "export_type": "File Sequence", + "ext": "exr", + "xml_preset_file": "OpenEXR (16-bit fp DWAA).xml", + "colorspace_out": "ACES - ACEScg", + "xml_preset_dir": "", + "parsed_comment_attrs": True, + "representation_add_range": True, + "representation_tags": [], + "load_to_batch_group": True, + "batch_group_loader_name": "LoadClipBatch", + "filter_path_regex": ".*" + } + ] + }, + "IntegrateBatchGroup": { + "enabled": False + } +} diff --git a/server_addon/flame/server/version.py b/server_addon/flame/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/flame/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/fusion/server/__init__.py b/server_addon/fusion/server/__init__.py new file mode 100644 index 0000000000..4d43f28812 --- /dev/null +++ b/server_addon/fusion/server/__init__.py @@ -0,0 +1,19 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import FusionSettings, DEFAULT_VALUES + + +class FusionAddon(BaseServerAddon): + name = "fusion" + title = "Fusion" + version = __version__ + settings_model: Type[FusionSettings] = FusionSettings + frontend_scopes = {} + services = {} + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/fusion/server/imageio.py b/server_addon/fusion/server/imageio.py new file mode 100644 index 0000000000..fe867af424 --- /dev/null +++ b/server_addon/fusion/server/imageio.py @@ -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 FusionImageIOModel(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" + ) diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py new file mode 100644 index 0000000000..92fb362c66 --- /dev/null +++ b/server_addon/fusion/server/settings.py @@ -0,0 +1,95 @@ +from pydantic import Field +from ayon_server.settings import ( + BaseSettingsModel, +) + +from .imageio import FusionImageIOModel + + +class CopyFusionSettingsModel(BaseSettingsModel): + copy_path: str = Field("", title="Local Fusion profile directory") + copy_status: bool = Field(title="Copy profile on first launch") + force_sync: bool = Field(title="Resync profile on each launch") + + +def _create_saver_instance_attributes_enum(): + return [ + { + "value": "reviewable", + "label": "Reviewable" + }, + { + "value": "farm_rendering", + "label": "Farm rendering" + } + ] + + +class CreateSaverPluginModel(BaseSettingsModel): + _isGroup = True + temp_rendering_path_template: str = Field( + "", title="Temporary rendering path template" + ) + default_variants: list[str] = Field( + default_factory=list, + title="Default variants" + ) + instance_attributes: list[str] = Field( + default_factory=list, + enum_resolver=_create_saver_instance_attributes_enum, + title="Instance attributes" + ) + + +class CreatPluginsModel(BaseSettingsModel): + CreateSaver: CreateSaverPluginModel = Field( + default_factory=CreateSaverPluginModel, + title="Create Saver" + ) + + +class FusionSettings(BaseSettingsModel): + imageio: FusionImageIOModel = Field( + default_factory=FusionImageIOModel, + title="Color Management (ImageIO)" + ) + copy_fusion_settings: CopyFusionSettingsModel = Field( + default_factory=CopyFusionSettingsModel, + title="Local Fusion profile settings" + ) + create: CreatPluginsModel = Field( + default_factory=CreatPluginsModel, + title="Creator plugins" + ) + + +DEFAULT_VALUES = { + "imageio": { + "ocio_config": { + "enabled": False, + "filepath": [] + }, + "file_rules": { + "enabled": False, + "rules": [] + } + }, + "copy_fusion_settings": { + "copy_path": "~/.openpype/hosts/fusion/profiles", + "copy_status": False, + "force_sync": False + }, + "create": { + "CreateSaver": { + "temp_rendering_path_template": "{workdir}/renders/fusion/{product[name]}/{product[name]}.{frame}.{ext}", + "default_variants": [ + "Main", + "Mask" + ], + "instance_attributes": [ + "reviewable", + "farm_rendering" + ] + } + } +} diff --git a/server_addon/fusion/server/version.py b/server_addon/fusion/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/fusion/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/harmony/LICENSE b/server_addon/harmony/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/server_addon/harmony/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/server_addon/harmony/README.md b/server_addon/harmony/README.md new file mode 100644 index 0000000000..d971fa39f9 --- /dev/null +++ b/server_addon/harmony/README.md @@ -0,0 +1,4 @@ +ToonBoom Harmony Addon +=============== + +Integration with ToonBoom Harmony. diff --git a/server_addon/harmony/server/__init__.py b/server_addon/harmony/server/__init__.py new file mode 100644 index 0000000000..64f41849ad --- /dev/null +++ b/server_addon/harmony/server/__init__.py @@ -0,0 +1,15 @@ +from ayon_server.addons import BaseServerAddon + +from .settings import HarmonySettings, DEFAULT_HARMONY_SETTING +from .version import __version__ + + +class Harmony(BaseServerAddon): + name = "harmony" + version = __version__ + + settings_model = HarmonySettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_HARMONY_SETTING) diff --git a/server_addon/harmony/server/settings/__init__.py b/server_addon/harmony/server/settings/__init__.py new file mode 100644 index 0000000000..4a8118d4da --- /dev/null +++ b/server_addon/harmony/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + HarmonySettings, + DEFAULT_HARMONY_SETTING, +) + + +__all__ = ( + "HarmonySettings", + "DEFAULT_HARMONY_SETTING", +) diff --git a/server_addon/harmony/server/settings/imageio.py b/server_addon/harmony/server/settings/imageio.py new file mode 100644 index 0000000000..4e01fae3d4 --- /dev/null +++ b/server_addon/harmony/server/settings/imageio.py @@ -0,0 +1,55 @@ +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 ImageIORemappingRulesModel(BaseSettingsModel): + host_native_name: str = Field( + title="Application native colorspace name" + ) + ocio_name: str = Field(title="OCIO colorspace name") + + +class HarmonyImageIOModel(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" + ) diff --git a/server_addon/harmony/server/settings/load.py b/server_addon/harmony/server/settings/load.py new file mode 100644 index 0000000000..1222485ff9 --- /dev/null +++ b/server_addon/harmony/server/settings/load.py @@ -0,0 +1,20 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class ImageSequenceLoaderModel(BaseSettingsModel): + family: list[str] = Field( + default_factory=list, + title="Families" + ) + representations: list[str] = Field( + default_factory=list, + title="Representations" + ) + + +class HarmonyLoadModel(BaseSettingsModel): + ImageSequenceLoader: ImageSequenceLoaderModel = Field( + default_factory=ImageSequenceLoaderModel, + title="Load Image Sequence" + ) diff --git a/server_addon/harmony/server/settings/main.py b/server_addon/harmony/server/settings/main.py new file mode 100644 index 0000000000..ae08da0198 --- /dev/null +++ b/server_addon/harmony/server/settings/main.py @@ -0,0 +1,68 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + +from .imageio import HarmonyImageIOModel +from .load import HarmonyLoadModel +from .publish_plugins import HarmonyPublishPlugins + + +class HarmonySettings(BaseSettingsModel): + """Harmony Project Settings.""" + + imageio: HarmonyImageIOModel = Field( + default_factory=HarmonyImageIOModel, + title="OCIO config" + ) + load: HarmonyLoadModel = Field( + default_factory=HarmonyLoadModel, + title="Loader plugins" + ) + publish: HarmonyPublishPlugins = Field( + default_factory=HarmonyPublishPlugins, + title="Publish plugins" + ) + + +DEFAULT_HARMONY_SETTING = { + "load": { + "ImageSequenceLoader": { + "family": [ + "shot", + "render", + "image", + "plate", + "reference" + ], + "representations": [ + "jpeg", + "png", + "jpg" + ] + } + }, + "publish": { + "CollectPalettes": { + "allowed_tasks": [ + ".*" + ] + }, + "ValidateAudio": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateContainers": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateSceneSettings": { + "enabled": True, + "optional": True, + "active": True, + "frame_check_filter": [], + "skip_resolution_check": [], + "skip_timelines_check": [] + } + } +} diff --git a/server_addon/harmony/server/settings/publish_plugins.py b/server_addon/harmony/server/settings/publish_plugins.py new file mode 100644 index 0000000000..bdaec2bbd4 --- /dev/null +++ b/server_addon/harmony/server/settings/publish_plugins.py @@ -0,0 +1,76 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class CollectPalettesPlugin(BaseSettingsModel): + """Set regular expressions to filter triggering on specific task names. '.*' means on all.""" # noqa + + allowed_tasks: list[str] = Field( + default_factory=list, + title="Allowed tasks" + ) + + +class ValidateAudioPlugin(BaseSettingsModel): + """Check if scene contains audio track.""" # + _isGroup = True + enabled: bool = True + optional: bool = Field(False, title="Optional") + active: bool = Field(True, title="Active") + + +class ValidateContainersPlugin(BaseSettingsModel): + """Check if loaded container is scene are latest versions.""" + _isGroup = True + enabled: bool = True + optional: bool = Field(False, title="Optional") + active: bool = Field(True, title="Active") + + +class ValidateSceneSettingsPlugin(BaseSettingsModel): + """Validate if FrameStart, FrameEnd and Resolution match shot data in DB. + Use regular expressions to limit validations only on particular asset + or task names.""" + _isGroup = True + enabled: bool = True + optional: bool = Field(False, title="Optional") + active: bool = Field(True, title="Active") + + frame_check_filter: list[str] = Field( + default_factory=list, + title="Skip Frame check for Assets with name containing" + ) + + skip_resolution_check: list[str] = Field( + default_factory=list, + title="Skip Resolution Check for Tasks" + ) + + skip_timelines_check: list[str] = Field( + default_factory=list, + title="Skip Timeline Check for Tasks" + ) + + +class HarmonyPublishPlugins(BaseSettingsModel): + + CollectPalettes: CollectPalettesPlugin = Field( + title="Collect Palettes", + default_factory=CollectPalettesPlugin, + ) + + ValidateAudio: ValidateAudioPlugin = Field( + title="Validate Audio", + default_factory=ValidateAudioPlugin, + ) + + ValidateContainers: ValidateContainersPlugin = Field( + title="Validate Containers", + default_factory=ValidateContainersPlugin, + ) + + ValidateSceneSettings: ValidateSceneSettingsPlugin = Field( + title="Validate Scene Settings", + default_factory=ValidateSceneSettingsPlugin, + ) diff --git a/server_addon/harmony/server/version.py b/server_addon/harmony/server/version.py new file mode 100644 index 0000000000..a242f0e757 --- /dev/null +++ b/server_addon/harmony/server/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Package declaring addon version.""" +__version__ = "0.1.1" diff --git a/server_addon/hiero/server/__init__.py b/server_addon/hiero/server/__init__.py new file mode 100644 index 0000000000..d0f9bcefc3 --- /dev/null +++ b/server_addon/hiero/server/__init__.py @@ -0,0 +1,19 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import HieroSettings, DEFAULT_VALUES + + +class HieroAddon(BaseServerAddon): + name = "hiero" + title = "Hiero" + version = __version__ + settings_model: Type[HieroSettings] = HieroSettings + frontend_scopes = {} + services = {} + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/hiero/server/settings/__init__.py b/server_addon/hiero/server/settings/__init__.py new file mode 100644 index 0000000000..246c8203e9 --- /dev/null +++ b/server_addon/hiero/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + HieroSettings, + DEFAULT_VALUES, +) + + +__all__ = ( + "HieroSettings", + "DEFAULT_VALUES", +) diff --git a/server_addon/hiero/server/settings/common.py b/server_addon/hiero/server/settings/common.py new file mode 100644 index 0000000000..eb4791f93e --- /dev/null +++ b/server_addon/hiero/server/settings/common.py @@ -0,0 +1,98 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel +from ayon_server.types import ( + ColorRGBA_float, + ColorRGB_uint8 +) + + +class Vector2d(BaseSettingsModel): + _layout = "compact" + + x: float = Field(1.0, title="X") + y: float = Field(1.0, title="Y") + + +class Vector3d(BaseSettingsModel): + _layout = "compact" + + x: float = Field(1.0, title="X") + y: float = Field(1.0, title="Y") + z: float = Field(1.0, title="Z") + + +def formatable_knob_type_enum(): + return [ + {"value": "text", "label": "Text"}, + {"value": "number", "label": "Number"}, + {"value": "decimal_number", "label": "Decimal number"}, + {"value": "2d_vector", "label": "2D vector"}, + # "3D vector" + ] + + +class Formatable(BaseSettingsModel): + _layout = "compact" + + template: str = Field( + "", + placeholder="""{{key}} or {{key}};{{key}}""", + title="Template" + ) + to_type: str = Field( + "Text", + title="To Knob type", + enum_resolver=formatable_knob_type_enum, + ) + + +knob_types_enum = [ + {"value": "text", "label": "Text"}, + {"value": "formatable", "label": "Formate from template"}, + {"value": "color_gui", "label": "Color GUI"}, + {"value": "boolean", "label": "Boolean"}, + {"value": "number", "label": "Number"}, + {"value": "decimal_number", "label": "Decimal number"}, + {"value": "vector_2d", "label": "2D vector"}, + {"value": "vector_3d", "label": "3D vector"}, + {"value": "color", "label": "Color"} +] + + +class KnobModel(BaseSettingsModel): + _layout = "expanded" + + type: str = Field( + title="Type", + description="Switch between different knob types", + enum_resolver=lambda: knob_types_enum, + conditionalEnum=True + ) + name: str = Field( + title="Name", + placeholder="Name" + ) + text: str = Field("", title="Value") + color_gui: ColorRGB_uint8 = Field( + (0, 0, 255), + title="RGB Uint8", + ) + boolean: bool = Field(False, title="Value") + number: int = Field(0, title="Value") + decimal_number: float = Field(0.0, title="Value") + vector_2d: Vector2d = Field( + default_factory=Vector2d, + title="Value" + ) + vector_3d: Vector3d = Field( + default_factory=Vector3d, + title="Value" + ) + color: ColorRGBA_float = Field( + (0.0, 0.0, 1.0, 1.0), + title="RGBA Float" + ) + formatable: Formatable = Field( + default_factory=Formatable, + title="Value" + ) diff --git a/server_addon/hiero/server/settings/create_plugins.py b/server_addon/hiero/server/settings/create_plugins.py new file mode 100644 index 0000000000..daec4a7cea --- /dev/null +++ b/server_addon/hiero/server/settings/create_plugins.py @@ -0,0 +1,97 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class CreateShotClipModels(BaseSettingsModel): + hierarchy: str = Field( + "{folder}/{sequence}", + title="Shot parent hierarchy", + section="Shot Hierarchy And Rename Settings" + ) + clipRename: bool = Field( + True, + title="Rename clips" + ) + clipName: str = Field( + "{track}{sequence}{shot}", + title="Clip name template" + ) + countFrom: int = Field( + 10, + title="Count sequence from" + ) + countSteps: int = Field( + 10, + title="Stepping number" + ) + + folder: str = Field( + "shots", + title="{folder}", + section="Shot Template Keywords" + ) + episode: str = Field( + "ep01", + title="{episode}" + ) + sequence: str = Field( + "sq01", + title="{sequence}" + ) + track: str = Field( + "{_track_}", + title="{track}" + ) + shot: str = Field( + "sh###", + title="{shot}" + ) + + vSyncOn: bool = Field( + False, + title="Enable Vertical Sync", + section="Vertical Synchronization Of Attributes" + ) + + workfileFrameStart: int = Field( + 1001, + title="Workfiles Start Frame", + section="Shot Attributes" + ) + handleStart: int = Field( + 10, + title="Handle start (head)" + ) + handleEnd: int = Field( + 10, + title="Handle end (tail)" + ) + + +class CreatorPluginsSettings(BaseSettingsModel): + CreateShotClip: CreateShotClipModels = Field( + default_factory=CreateShotClipModels, + title="Create Shot Clip" + ) + + +DEFAULT_CREATE_SETTINGS = { + "create": { + "CreateShotClip": { + "hierarchy": "{folder}/{sequence}", + "clipRename": True, + "clipName": "{track}{sequence}{shot}", + "countFrom": 10, + "countSteps": 10, + "folder": "shots", + "episode": "ep01", + "sequence": "sq01", + "track": "{_track_}", + "shot": "sh###", + "vSyncOn": False, + "workfileFrameStart": 1001, + "handleStart": 10, + "handleEnd": 10 + } + } +} diff --git a/server_addon/hiero/server/settings/filters.py b/server_addon/hiero/server/settings/filters.py new file mode 100644 index 0000000000..7e2702b3b7 --- /dev/null +++ b/server_addon/hiero/server/settings/filters.py @@ -0,0 +1,19 @@ +from pydantic import Field, validator +from ayon_server.settings import BaseSettingsModel, ensure_unique_names + + +class PublishGUIFilterItemModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: bool = Field(True, title="Active") + + +class PublishGUIFiltersModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: list[PublishGUIFilterItemModel] = Field(default_factory=list) + + @validator("value") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value diff --git a/server_addon/hiero/server/settings/imageio.py b/server_addon/hiero/server/settings/imageio.py new file mode 100644 index 0000000000..f2c2728057 --- /dev/null +++ b/server_addon/hiero/server/settings/imageio.py @@ -0,0 +1,169 @@ +from pydantic import Field, validator + +from ayon_server.settings import ( + BaseSettingsModel, + ensure_unique_names, +) + + +def ocio_configs_switcher_enum(): + return [ + {"value": "nuke-default", "label": "nuke-default"}, + {"value": "spi-vfx", "label": "spi-vfx"}, + {"value": "spi-anim", "label": "spi-anim"}, + {"value": "aces_0.1.1", "label": "aces_0.1.1"}, + {"value": "aces_0.7.1", "label": "aces_0.7.1"}, + {"value": "aces_1.0.1", "label": "aces_1.0.1"}, + {"value": "aces_1.0.3", "label": "aces_1.0.3"}, + {"value": "aces_1.1", "label": "aces_1.1"}, + {"value": "aces_1.2", "label": "aces_1.2"}, + {"value": "aces_1.3", "label": "aces_1.3"}, + {"value": "custom", "label": "custom"} + ] + + +class WorkfileColorspaceSettings(BaseSettingsModel): + """Hiero workfile colorspace preset. """ + """# TODO: enhance settings with host api: + we need to add mapping to resolve properly keys. + Hiero is excpecting camel case key names, + but for better code consistency we are using snake_case: + + ocio_config = ocioConfigName + working_space_name = workingSpace + int_16_name = sixteenBitLut + int_8_name = eightBitLut + float_name = floatLut + log_name = logLut + viewer_name = viewerLut + thumbnail_name = thumbnailLut + """ + + ocioConfigName: str = Field( + title="OpenColorIO Config", + description="Switch between OCIO configs", + enum_resolver=ocio_configs_switcher_enum, + conditionalEnum=True + ) + workingSpace: str = Field( + title="Working Space" + ) + viewerLut: str = Field( + title="Viewer" + ) + eightBitLut: str = Field( + title="8-bit files" + ) + sixteenBitLut: str = Field( + title="16-bit files" + ) + logLut: str = Field( + title="Log files" + ) + floatLut: str = Field( + title="Float files" + ) + thumbnailLut: str = Field( + title="Thumnails" + ) + monitorOutLut: str = Field( + title="Monitor" + ) + + +class ClipColorspaceRulesItems(BaseSettingsModel): + _layout = "expanded" + + regex: str = Field("", title="Regex expression") + colorspace: str = Field("", title="Colorspace") + + +class RegexInputsModel(BaseSettingsModel): + inputs: list[ClipColorspaceRulesItems] = Field( + default_factory=list, + title="Inputs" + ) + + +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): + """Hiero color management project settings. """ + _isGroup: bool = True + 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" + ) + workfile: WorkfileColorspaceSettings = Field( + default_factory=WorkfileColorspaceSettings, + title="Workfile" + ) + """# TODO: enhance settings with host api: + - old settings are using `regexInputs` key but we + need to rename to `regex_inputs` + - no need for `inputs` middle part. It can stay + directly on `regex_inputs` + """ + regexInputs: RegexInputsModel = Field( + default_factory=RegexInputsModel, + title="Assign colorspace to clips via rules" + ) + + +DEFAULT_IMAGEIO_SETTINGS = { + "workfile": { + "ocioConfigName": "nuke-default", + "workingSpace": "linear", + "viewerLut": "sRGB", + "eightBitLut": "sRGB", + "sixteenBitLut": "sRGB", + "logLut": "Cineon", + "floatLut": "linear", + "thumbnailLut": "sRGB", + "monitorOutLut": "sRGB" + }, + "regexInputs": { + "inputs": [ + { + "regex": "[^-a-zA-Z0-9](plateRef).*(?=mp4)", + "colorspace": "sRGB" + } + ] + } +} diff --git a/server_addon/hiero/server/settings/loader_plugins.py b/server_addon/hiero/server/settings/loader_plugins.py new file mode 100644 index 0000000000..83b3564c2a --- /dev/null +++ b/server_addon/hiero/server/settings/loader_plugins.py @@ -0,0 +1,38 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class LoadClipModel(BaseSettingsModel): + enabled: bool = Field( + True, + title="Enabled" + ) + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + clip_name_template: str = Field( + title="Clip name template" + ) + + +class LoaderPuginsModel(BaseSettingsModel): + LoadClip: LoadClipModel = Field( + default_factory=LoadClipModel, + title="Load Clip" + ) + + +DEFAULT_LOADER_PLUGINS_SETTINGS = { + "LoadClip": { + "enabled": True, + "product_types": [ + "render2d", + "source", + "plate", + "render", + "review" + ], + "clip_name_template": "{folder[name]}_{product[name]}_{representation}" + } +} diff --git a/server_addon/hiero/server/settings/main.py b/server_addon/hiero/server/settings/main.py new file mode 100644 index 0000000000..47f8110c22 --- /dev/null +++ b/server_addon/hiero/server/settings/main.py @@ -0,0 +1,64 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + +from .imageio import ( + ImageIOSettings, + DEFAULT_IMAGEIO_SETTINGS +) +from .create_plugins import ( + CreatorPluginsSettings, + DEFAULT_CREATE_SETTINGS +) +from .loader_plugins import ( + LoaderPuginsModel, + DEFAULT_LOADER_PLUGINS_SETTINGS +) +from .publish_plugins import ( + PublishPuginsModel, + DEFAULT_PUBLISH_PLUGIN_SETTINGS +) +from .scriptsmenu import ( + ScriptsmenuSettings, + DEFAULT_SCRIPTSMENU_SETTINGS +) +from .filters import PublishGUIFilterItemModel + + +class HieroSettings(BaseSettingsModel): + """Nuke addon settings.""" + + imageio: ImageIOSettings = Field( + default_factory=ImageIOSettings, + title="Color Management (imageio)", + ) + + create: CreatorPluginsSettings = Field( + default_factory=CreatorPluginsSettings, + title="Creator Plugins", + ) + load: LoaderPuginsModel = Field( + default_factory=LoaderPuginsModel, + title="Loader plugins" + ) + publish: PublishPuginsModel = Field( + default_factory=PublishPuginsModel, + title="Publish plugins" + ) + scriptsmenu: ScriptsmenuSettings = Field( + default_factory=ScriptsmenuSettings, + title="Scripts Menu Definition", + ) + filters: list[PublishGUIFilterItemModel] = Field( + default_factory=list + ) + + +DEFAULT_VALUES = { + "imageio": DEFAULT_IMAGEIO_SETTINGS, + "create": DEFAULT_CREATE_SETTINGS, + "load": DEFAULT_LOADER_PLUGINS_SETTINGS, + "publish": DEFAULT_PUBLISH_PLUGIN_SETTINGS, + "scriptsmenu": DEFAULT_SCRIPTSMENU_SETTINGS, + "filters": [], +} diff --git a/server_addon/hiero/server/settings/publish_plugins.py b/server_addon/hiero/server/settings/publish_plugins.py new file mode 100644 index 0000000000..a85e62724b --- /dev/null +++ b/server_addon/hiero/server/settings/publish_plugins.py @@ -0,0 +1,48 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class CollectInstanceVersionModel(BaseSettingsModel): + enabled: bool = Field( + True, + title="Enabled" + ) + + +class ExtractReviewCutUpVideoModel(BaseSettingsModel): + enabled: bool = Field( + True, + title="Enabled" + ) + tags_addition: list[str] = Field( + default_factory=list, + title="Additional tags" + ) + + +class PublishPuginsModel(BaseSettingsModel): + CollectInstanceVersion: CollectInstanceVersionModel = Field( + default_factory=CollectInstanceVersionModel, + title="Collect Instance Version" + ) + """# TODO: enhance settings with host api: + Rename class name and plugin name + to match title (it makes more sense) + """ + ExtractReviewCutUpVideo: ExtractReviewCutUpVideoModel = Field( + default_factory=ExtractReviewCutUpVideoModel, + title="Exctract Review Trim" + ) + + +DEFAULT_PUBLISH_PLUGIN_SETTINGS = { + "CollectInstanceVersion": { + "enabled": False, + }, + "ExtractReviewCutUpVideo": { + "enabled": True, + "tags_addition": [ + "review" + ] + } +} diff --git a/server_addon/hiero/server/settings/scriptsmenu.py b/server_addon/hiero/server/settings/scriptsmenu.py new file mode 100644 index 0000000000..51cb088298 --- /dev/null +++ b/server_addon/hiero/server/settings/scriptsmenu.py @@ -0,0 +1,41 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class ScriptsmenuSubmodel(BaseSettingsModel): + """Item Definition""" + _isGroup = True + + type: str = Field(title="Type") + command: str = Field(title="Command") + sourcetype: str = Field(title="Source Type") + title: str = Field(title="Title") + tooltip: str = Field(title="Tooltip") + + +class ScriptsmenuSettings(BaseSettingsModel): + """Nuke script menu project settings.""" + _isGroup = True + + """# TODO: enhance settings with host api: + - in api rename key `name` to `menu_name` + """ + name: str = Field(title="Menu name") + definition: list[ScriptsmenuSubmodel] = Field( + default_factory=list, + title="Definition", + description="Scriptmenu Items Definition") + + +DEFAULT_SCRIPTSMENU_SETTINGS = { + "name": "OpenPype Tools", + "definition": [ + { + "type": "action", + "sourcetype": "python", + "title": "OpenPype Docs", + "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_hiero')", + "tooltip": "Open the OpenPype Hiero user doc page" + } + ] +} diff --git a/server_addon/hiero/server/version.py b/server_addon/hiero/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/hiero/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/houdini/server/__init__.py b/server_addon/houdini/server/__init__.py new file mode 100644 index 0000000000..870ec2d0b7 --- /dev/null +++ b/server_addon/houdini/server/__init__.py @@ -0,0 +1,17 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import HoudiniSettings, DEFAULT_VALUES + + +class Houdini(BaseServerAddon): + name = "houdini" + title = "Houdini" + version = __version__ + settings_model: Type[HoudiniSettings] = HoudiniSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/houdini/server/settings/__init__.py b/server_addon/houdini/server/settings/__init__.py new file mode 100644 index 0000000000..9fd2678925 --- /dev/null +++ b/server_addon/houdini/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + HoudiniSettings, + DEFAULT_VALUES, +) + + +__all__ = ( + "HoudiniSettings", + "DEFAULT_VALUES", +) diff --git a/server_addon/houdini/server/settings/imageio.py b/server_addon/houdini/server/settings/imageio.py new file mode 100644 index 0000000000..88aa40ecd6 --- /dev/null +++ b/server_addon/houdini/server/settings/imageio.py @@ -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 HoudiniImageIOModel(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" + ) diff --git a/server_addon/houdini/server/settings/main.py b/server_addon/houdini/server/settings/main.py new file mode 100644 index 0000000000..fdb6838f5c --- /dev/null +++ b/server_addon/houdini/server/settings/main.py @@ -0,0 +1,79 @@ +from pydantic import Field +from ayon_server.settings import ( + BaseSettingsModel, + MultiplatformPathModel, + MultiplatformPathListModel, +) + +from .imageio import HoudiniImageIOModel +from .publish_plugins import ( + PublishPluginsModel, + CreatePluginsModel, + DEFAULT_HOUDINI_PUBLISH_SETTINGS, + DEFAULT_HOUDINI_CREATE_SETTINGS +) + + +class ShelfToolsModel(BaseSettingsModel): + name: str = Field(title="Name") + help: str = Field(title="Help text") + script: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Script Path " + ) + icon: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Icon Path " + ) + + +class ShelfDefinitionModel(BaseSettingsModel): + _layout = "expanded" + shelf_name: str = Field(title="Shelf name") + tools_list: list[ShelfToolsModel] = Field( + default_factory=list, + title="Shelf Tools" + ) + + +class ShelvesModel(BaseSettingsModel): + _layout = "expanded" + shelf_set_name: str = Field(title="Shelfs set name") + + shelf_set_source_path: MultiplatformPathListModel = Field( + default_factory=MultiplatformPathListModel, + title="Shelf Set Path (optional)" + ) + + shelf_definition: list[ShelfDefinitionModel] = Field( + default_factory=list, + title="Shelf Definitions" + ) + + +class HoudiniSettings(BaseSettingsModel): + imageio: HoudiniImageIOModel = Field( + default_factory=HoudiniImageIOModel, + title="Color Management (ImageIO)" + ) + shelves: list[ShelvesModel] = Field( + default_factory=list, + title="Houdini Scripts Shelves", + ) + + publish: PublishPluginsModel = Field( + default_factory=PublishPluginsModel, + title="Publish Plugins", + ) + + create: CreatePluginsModel = Field( + default_factory=CreatePluginsModel, + title="Creator Plugins", + ) + + +DEFAULT_VALUES = { + "shelves": [], + "create": DEFAULT_HOUDINI_CREATE_SETTINGS, + "publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS +} diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py new file mode 100644 index 0000000000..ca5d0a4ea5 --- /dev/null +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -0,0 +1,150 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +# Creator Plugins +class CreatorModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + defaults: list[str] = Field(title="Default Products") + + +class CreateArnoldAssModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + defaults: list[str] = Field(title="Default Products") + ext: str = Field(Title="Extension") + + +class CreatePluginsModel(BaseSettingsModel): + CreateArnoldAss: CreateArnoldAssModel = Field( + default_factory=CreateArnoldAssModel, + title="Create Alembic Camera") + CreateAlembicCamera: CreatorModel = Field( + default_factory=CreatorModel, + title="Create Alembic Camera") + CreateCompositeSequence: CreatorModel = Field( + default_factory=CreatorModel, + title="Create Composite Sequence") + CreatePointCache: CreatorModel = Field( + default_factory=CreatorModel, + title="Create Point Cache") + CreateRedshiftROP: CreatorModel = Field( + default_factory=CreatorModel, + title="Create RedshiftROP") + CreateRemotePublish: CreatorModel = Field( + default_factory=CreatorModel, + title="Create Remote Publish") + CreateVDBCache: CreatorModel = Field( + default_factory=CreatorModel, + title="Create VDB Cache") + CreateUSD: CreatorModel = Field( + default_factory=CreatorModel, + title="Create USD") + CreateUSDModel: CreatorModel = Field( + default_factory=CreatorModel, + title="Create USD model") + USDCreateShadingWorkspace: CreatorModel = Field( + default_factory=CreatorModel, + title="Create USD shading workspace") + CreateUSDRender: CreatorModel = Field( + default_factory=CreatorModel, + title="Create USD render") + + +DEFAULT_HOUDINI_CREATE_SETTINGS = { + "CreateArnoldAss": { + "enabled": True, + "defaults": [], + "ext": ".ass" + }, + "CreateAlembicCamera": { + "enabled": True, + "defaults": [] + }, + "CreateCompositeSequence": { + "enabled": True, + "defaults": [] + }, + "CreatePointCache": { + "enabled": True, + "defaults": [] + }, + "CreateRedshiftROP": { + "enabled": True, + "defaults": [] + }, + "CreateRemotePublish": { + "enabled": True, + "defaults": [] + }, + "CreateVDBCache": { + "enabled": True, + "defaults": [] + }, + "CreateUSD": { + "enabled": False, + "defaults": [] + }, + "CreateUSDModel": { + "enabled": False, + "defaults": [] + }, + "USDCreateShadingWorkspace": { + "enabled": False, + "defaults": [] + }, + "CreateUSDRender": { + "enabled": False, + "defaults": [] + } +} + + +# Publish Plugins +class ValidateWorkfilePathsModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + node_types: list[str] = Field( + default_factory=list, + title="Node Types" + ) + prohibited_vars: list[str] = Field( + default_factory=list, + title="Prohibited Variables" + ) + + +class ValidateContainersModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + + +class PublishPluginsModel(BaseSettingsModel): + ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( + default_factory=ValidateWorkfilePathsModel, + title="Validate workfile paths settings.") + ValidateContainers: ValidateContainersModel = Field( + default_factory=ValidateContainersModel, + title="Validate Latest Containers.") + + +DEFAULT_HOUDINI_PUBLISH_SETTINGS = { + "ValidateWorkfilePaths": { + "enabled": True, + "optional": True, + "node_types": [ + "file", + "alembic" + ], + "prohibited_vars": [ + "$HIP", + "$JOB" + ] + }, + "ValidateContainers": { + "enabled": True, + "optional": True, + "active": True + } +} diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/houdini/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/kitsu/server/__init__.py b/server_addon/kitsu/server/__init__.py new file mode 100644 index 0000000000..69cf812dea --- /dev/null +++ b/server_addon/kitsu/server/__init__.py @@ -0,0 +1,19 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import KitsuSettings, DEFAULT_VALUES + + +class KitsuAddon(BaseServerAddon): + name = "kitsu" + title = "Kitsu" + version = __version__ + settings_model: Type[KitsuSettings] = KitsuSettings + frontend_scopes = {} + services = {} + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/kitsu/server/settings.py b/server_addon/kitsu/server/settings.py new file mode 100644 index 0000000000..7afa73ec72 --- /dev/null +++ b/server_addon/kitsu/server/settings.py @@ -0,0 +1,111 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class EntityPattern(BaseSettingsModel): + episode: str = Field(title="Episode") + sequence: str = Field(title="Sequence") + shot: str = Field(title="Shot") + + +def _status_change_cond_enum(): + return [ + {"value": "equal", "label": "Equal"}, + {"value": "not_equal", "label": "Not equal"} + ] + + +class StatusChangeCondition(BaseSettingsModel): + condition: str = Field( + "equal", + enum_resolver=_status_change_cond_enum, + title="Condition" + ) + short_name: str = Field("", title="Short name") + + +class StatusChangeProductTypeRequirementModel(BaseSettingsModel): + condition: str = Field( + "equal", + enum_resolver=_status_change_cond_enum, + title="Condition" + ) + product_type: str = Field("", title="Product type") + + +class StatusChangeConditionsModel(BaseSettingsModel): + status_conditions: list[StatusChangeCondition] = Field( + default_factory=list, + title="Status conditions" + ) + product_type_requirements: list[StatusChangeProductTypeRequirementModel] = Field( + default_factory=list, + title="Product type requirements") + + +class CustomCommentTemplateModel(BaseSettingsModel): + enabled: bool = Field(True) + comment_template: str = Field("", title="Custom comment") + + +class IntegrateKitsuNotes(BaseSettingsModel): + """Kitsu supports markdown and here you can create a custom comment template. + + You can use data from your publishing instance's data. + """ + + set_status_note: bool = Field(title="Set status on note") + note_status_shortname: str = Field(title="Note shortname") + status_change_conditions: StatusChangeConditionsModel = Field( + default_factory=StatusChangeConditionsModel, + title="Status change conditions" + ) + custom_comment_template: CustomCommentTemplateModel = Field( + default_factory=CustomCommentTemplateModel, + title="Custom Comment Template", + ) + + +class PublishPlugins(BaseSettingsModel): + IntegrateKitsuNote: IntegrateKitsuNotes = Field( + default_factory=IntegrateKitsuNotes, + title="Integrate Kitsu Note" + ) + + +class KitsuSettings(BaseSettingsModel): + server: str = Field( + "", + title="Kitsu Server" + ) + entities_naming_pattern: EntityPattern = Field( + default_factory=EntityPattern, + title="Entities naming pattern" + ) + publish: PublishPlugins = Field( + default_factory=PublishPlugins, + title="Publish plugins" + ) + + +DEFAULT_VALUES = { + "entities_naming_pattern": { + "episode": "E##", + "sequence": "SQ##", + "shot": "SH##" + }, + "publish": { + "IntegrateKitsuNote": { + "set_status_note": False, + "note_status_shortname": "wfa", + "status_change_conditions": { + "status_conditions": [], + "product_type_requirements": [] + }, + "custom_comment_template": { + "enabled": False, + "comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| product type | `{product[type]}` |\n| name | `{name}` |" + } + } + } +} diff --git a/server_addon/kitsu/server/version.py b/server_addon/kitsu/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/kitsu/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/maya/LICENCE b/server_addon/maya/LICENCE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/server_addon/maya/LICENCE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/server_addon/maya/README.md b/server_addon/maya/README.md new file mode 100644 index 0000000000..c65c09fba0 --- /dev/null +++ b/server_addon/maya/README.md @@ -0,0 +1,4 @@ +Maya Integration Addon +====================== + +WIP diff --git a/server_addon/maya/server/__init__.py b/server_addon/maya/server/__init__.py new file mode 100644 index 0000000000..8784427dcf --- /dev/null +++ b/server_addon/maya/server/__init__.py @@ -0,0 +1,16 @@ +"""Maya Addon Module""" +from ayon_server.addons import BaseServerAddon + +from .settings.main import MayaSettings, DEFAULT_MAYA_SETTING +from .version import __version__ + + +class MayaAddon(BaseServerAddon): + name = "maya" + title = "Maya" + version = __version__ + settings_model = MayaSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_MAYA_SETTING) diff --git a/server_addon/maya/server/settings/__init__.py b/server_addon/maya/server/settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py new file mode 100644 index 0000000000..3756d45e6c --- /dev/null +++ b/server_addon/maya/server/settings/creators.py @@ -0,0 +1,408 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class CreateLookModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + make_tx: bool = Field(title="Make tx files") + rs_tex: bool = Field(title="Make Redshift texture files") + defaults: list[str] = Field( + default_factory=["Main"], title="Default Products" + ) + + +class BasicCreatorModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + defaults: list[str] = Field( + default_factory=list, + title="Default Products" + ) + + +class CreateUnrealStaticMeshModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + defaults: list[str] = Field( + default_factory=["", "_Main"], + title="Default Products" + ) + static_mesh_prefixes: str = Field("S", title="Static Mesh Prefix") + collision_prefixes: list[str] = Field( + default_factory=["UBX", "UCP", "USP", "UCX"], + title="Collision Prefixes" + ) + + +class CreateUnrealSkeletalMeshModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + defaults: list[str] = Field(default_factory=[], title="Default Products") + joint_hints: str = Field("jnt_org", title="Joint root hint") + + +class CreateMultiverseLookModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + publish_mip_map: bool = Field(title="publish_mip_map") + + +class BasicExportMeshModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + write_color_sets: bool = Field(title="Write Color Sets") + write_face_sets: bool = Field(title="Write Face Sets") + defaults: list[str] = Field( + default_factory=list, + title="Default Products" + ) + + +class CreateAnimationModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + write_color_sets: bool = Field(title="Write Color Sets") + write_face_sets: bool = Field(title="Write Face Sets") + include_parent_hierarchy: bool = Field( + title="Include Parent Hierarchy") + include_user_defined_attributes: bool = Field( + title="Include User Defined Attributes") + defaults: list[str] = Field( + default_factory=list, + title="Default Products" + ) + + +class CreatePointCacheModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + write_color_sets: bool = Field(title="Write Color Sets") + write_face_sets: bool = Field(title="Write Face Sets") + include_user_defined_attributes: bool = Field( + title="Include User Defined Attributes" + ) + defaults: list[str] = Field( + default_factory=["Main"], + title="Default Products" + ) + + +class CreateProxyAlembicModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + write_color_sets: bool = Field(title="Write Color Sets") + write_face_sets: bool = Field(title="Write Face Sets") + defaults: list[str] = Field( + default_factory=["Main"], + title="Default Products" + ) + + +class CreateAssModel(BasicCreatorModel): + expandProcedurals: bool = Field(title="Expand Procedurals") + motionBlur: bool = Field(title="Motion Blur") + motionBlurKeys: int = Field(2, title="Motion Blur Keys") + motionBlurLength: float = Field(0.5, title="Motion Blur Length") + maskOptions: bool = Field(title="Mask Options") + maskCamera: bool = Field(title="Mask Camera") + maskLight: bool = Field(title="Mask Light") + maskShape: bool = Field(title="Mask Shape") + maskShader: bool = Field(title="Mask Shader") + maskOverride: bool = Field(title="Mask Override") + maskDriver: bool = Field(title="Mask Driver") + maskFilter: bool = Field(title="Mask Filter") + maskColor_manager: bool = Field(title="Mask Color Manager") + maskOperator: bool = Field(title="Mask Operator") + + +class CreateReviewModel(BasicCreatorModel): + useMayaTimeline: bool = Field(title="Use Maya Timeline for Frame Range.") + + +class CreateVrayProxyModel(BaseSettingsModel): + enabled: bool = Field(True) + vrmesh: bool = Field(title="VrMesh") + alembic: bool = Field(title="Alembic") + defaults: list[str] = Field(default_factory=list, title="Default Products") + + +class CreatorsModel(BaseSettingsModel): + CreateLook: CreateLookModel = Field( + default_factory=CreateLookModel, + title="Create Look" + ) + CreateRender: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Render" + ) + # "-" is not compatible in the new model + CreateUnrealStaticMesh: CreateUnrealStaticMeshModel = Field( + default_factory=CreateUnrealStaticMeshModel, + title="Create Unreal_Static Mesh" + ) + # "-" is not compatible in the new model + CreateUnrealSkeletalMesh: CreateUnrealSkeletalMeshModel = Field( + default_factory=CreateUnrealSkeletalMeshModel, + title="Create Unreal_Skeletal Mesh" + ) + CreateMultiverseLook: CreateMultiverseLookModel = Field( + default_factory=CreateMultiverseLookModel, + title="Create Multiverse Look" + ) + CreateAnimation: CreateAnimationModel = Field( + default_factory=CreateAnimationModel, + title="Create Animation" + ) + CreateModel: BasicExportMeshModel = Field( + default_factory=BasicExportMeshModel, + title="Create Model" + ) + CreatePointCache: CreatePointCacheModel = Field( + default_factory=CreatePointCacheModel, + title="Create Point Cache" + ) + CreateProxyAlembic: CreateProxyAlembicModel = Field( + default_factory=CreateProxyAlembicModel, + title="Create Proxy Alembic" + ) + CreateMultiverseUsd: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Multiverse USD" + ) + CreateMultiverseUsdComp: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Multiverse USD Composition" + ) + CreateMultiverseUsdOver: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Multiverse USD Override" + ) + CreateAss: CreateAssModel = Field( + default_factory=CreateAssModel, + title="Create Ass" + ) + CreateAssembly: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Assembly" + ) + CreateCamera: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Camera" + ) + CreateLayout: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Layout" + ) + CreateMayaScene: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Maya Scene" + ) + CreateRenderSetup: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Render Setup" + ) + CreateReview: CreateReviewModel = Field( + default_factory=CreateReviewModel, + title="Create Review" + ) + CreateRig: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Rig" + ) + CreateSetDress: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Set Dress" + ) + CreateVrayProxy: CreateVrayProxyModel = Field( + default_factory=CreateVrayProxyModel, + title="Create VRay Proxy" + ) + CreateVRayScene: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create VRay Scene" + ) + CreateYetiRig: BasicCreatorModel = Field( + default_factory=BasicCreatorModel, + title="Create Yeti Rig" + ) + + +DEFAULT_CREATORS_SETTINGS = { + "CreateLook": { + "enabled": True, + "make_tx": True, + "rs_tex": False, + "defaults": [ + "Main" + ] + }, + "CreateRender": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateUnrealStaticMesh": { + "enabled": True, + "defaults": [ + "", + "_Main" + ], + "static_mesh_prefix": "S", + "collision_prefixes": [ + "UBX", + "UCP", + "USP", + "UCX" + ] + }, + "CreateUnrealSkeletalMesh": { + "enabled": True, + "defaults": [], + "joint_hints": "jnt_org" + }, + "CreateMultiverseLook": { + "enabled": True, + "publish_mip_map": True + }, + "CreateAnimation": { + "enabled": False, + "write_color_sets": False, + "write_face_sets": False, + "include_parent_hierarchy": False, + "include_user_defined_attributes": False, + "defaults": [ + "Main" + ] + }, + "CreateModel": { + "enabled": True, + "write_color_sets": False, + "write_face_sets": False, + "defaults": [ + "Main", + "Proxy", + "Sculpt" + ] + }, + "CreatePointCache": { + "enabled": True, + "write_color_sets": False, + "write_face_sets": False, + "include_user_defined_attributes": False, + "defaults": [ + "Main" + ] + }, + "CreateProxyAlembic": { + "enabled": True, + "write_color_sets": False, + "write_face_sets": False, + "defaults": [ + "Main" + ] + }, + "CreateMultiverseUsd": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateMultiverseUsdComp": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateMultiverseUsdOver": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateAss": { + "enabled": True, + "defaults": [ + "Main" + ], + "expandProcedurals": False, + "motionBlur": True, + "motionBlurKeys": 2, + "motionBlurLength": 0.5, + "maskOptions": False, + "maskCamera": False, + "maskLight": False, + "maskShape": False, + "maskShader": False, + "maskOverride": False, + "maskDriver": False, + "maskFilter": False, + "maskColor_manager": False, + "maskOperator": False + }, + "CreateAssembly": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateCamera": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateLayout": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateMayaScene": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateRenderSetup": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateReview": { + "enabled": True, + "defaults": [ + "Main" + ], + "useMayaTimeline": True + }, + "CreateRig": { + "enabled": True, + "defaults": [ + "Main", + "Sim", + "Cloth" + ] + }, + "CreateSetDress": { + "enabled": True, + "defaults": [ + "Main", + "Anim" + ] + }, + "CreateVrayProxy": { + "enabled": True, + "vrmesh": True, + "alembic": True, + "defaults": [ + "Main" + ] + }, + "CreateVRayScene": { + "enabled": True, + "defaults": [ + "Main" + ] + }, + "CreateYetiRig": { + "enabled": True, + "defaults": [ + "Main" + ] + } +} diff --git a/server_addon/maya/server/settings/explicit_plugins_loading.py b/server_addon/maya/server/settings/explicit_plugins_loading.py new file mode 100644 index 0000000000..394adb728f --- /dev/null +++ b/server_addon/maya/server/settings/explicit_plugins_loading.py @@ -0,0 +1,429 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class PluginsModel(BaseSettingsModel): + _layout = "expanded" + enabled: bool = Field(title="Enabled") + name: str = Field("", title="Name") + + +class ExplicitPluginsLoadingModel(BaseSettingsModel): + """Maya Explicit Plugins Loading.""" + _isGroup: bool = True + enabled: bool = Field(title="enabled") + plugins_to_load: list[PluginsModel] = Field( + default_factory=list, title="Plugins To Load" + ) + + +DEFAULT_EXPLITCIT_PLUGINS_LOADING_SETTINGS = { + "enabled": False, + "plugins_to_load": [ + { + "enabled": False, + "name": "AbcBullet" + }, + { + "enabled": True, + "name": "AbcExport" + }, + { + "enabled": True, + "name": "AbcImport" + }, + { + "enabled": False, + "name": "animImportExport" + }, + { + "enabled": False, + "name": "ArubaTessellator" + }, + { + "enabled": False, + "name": "ATFPlugin" + }, + { + "enabled": False, + "name": "atomImportExport" + }, + { + "enabled": False, + "name": "AutodeskPacketFile" + }, + { + "enabled": False, + "name": "autoLoader" + }, + { + "enabled": False, + "name": "bifmeshio" + }, + { + "enabled": False, + "name": "bifrostGraph" + }, + { + "enabled": False, + "name": "bifrostshellnode" + }, + { + "enabled": False, + "name": "bifrostvisplugin" + }, + { + "enabled": False, + "name": "blast2Cmd" + }, + { + "enabled": False, + "name": "bluePencil" + }, + { + "enabled": False, + "name": "Boss" + }, + { + "enabled": False, + "name": "bullet" + }, + { + "enabled": True, + "name": "cacheEvaluator" + }, + { + "enabled": False, + "name": "cgfxShader" + }, + { + "enabled": False, + "name": "cleanPerFaceAssignment" + }, + { + "enabled": False, + "name": "clearcoat" + }, + { + "enabled": False, + "name": "convertToComponentTags" + }, + { + "enabled": False, + "name": "curveWarp" + }, + { + "enabled": False, + "name": "ddsFloatReader" + }, + { + "enabled": True, + "name": "deformerEvaluator" + }, + { + "enabled": False, + "name": "dgProfiler" + }, + { + "enabled": False, + "name": "drawUfe" + }, + { + "enabled": False, + "name": "dx11Shader" + }, + { + "enabled": False, + "name": "fbxmaya" + }, + { + "enabled": False, + "name": "fltTranslator" + }, + { + "enabled": False, + "name": "freeze" + }, + { + "enabled": False, + "name": "Fur" + }, + { + "enabled": False, + "name": "gameFbxExporter" + }, + { + "enabled": False, + "name": "gameInputDevice" + }, + { + "enabled": False, + "name": "GamePipeline" + }, + { + "enabled": False, + "name": "gameVertexCount" + }, + { + "enabled": False, + "name": "geometryReport" + }, + { + "enabled": False, + "name": "geometryTools" + }, + { + "enabled": False, + "name": "glslShader" + }, + { + "enabled": True, + "name": "GPUBuiltInDeformer" + }, + { + "enabled": False, + "name": "gpuCache" + }, + { + "enabled": False, + "name": "hairPhysicalShader" + }, + { + "enabled": False, + "name": "ik2Bsolver" + }, + { + "enabled": False, + "name": "ikSpringSolver" + }, + { + "enabled": False, + "name": "invertShape" + }, + { + "enabled": False, + "name": "lges" + }, + { + "enabled": False, + "name": "lookdevKit" + }, + { + "enabled": False, + "name": "MASH" + }, + { + "enabled": False, + "name": "matrixNodes" + }, + { + "enabled": False, + "name": "mayaCharacterization" + }, + { + "enabled": False, + "name": "mayaHIK" + }, + { + "enabled": False, + "name": "MayaMuscle" + }, + { + "enabled": False, + "name": "mayaUsdPlugin" + }, + { + "enabled": False, + "name": "mayaVnnPlugin" + }, + { + "enabled": False, + "name": "melProfiler" + }, + { + "enabled": False, + "name": "meshReorder" + }, + { + "enabled": True, + "name": "modelingToolkit" + }, + { + "enabled": False, + "name": "mtoa" + }, + { + "enabled": False, + "name": "mtoh" + }, + { + "enabled": False, + "name": "nearestPointOnMesh" + }, + { + "enabled": True, + "name": "objExport" + }, + { + "enabled": False, + "name": "OneClick" + }, + { + "enabled": False, + "name": "OpenEXRLoader" + }, + { + "enabled": False, + "name": "pgYetiMaya" + }, + { + "enabled": False, + "name": "pgyetiVrayMaya" + }, + { + "enabled": False, + "name": "polyBoolean" + }, + { + "enabled": False, + "name": "poseInterpolator" + }, + { + "enabled": False, + "name": "quatNodes" + }, + { + "enabled": False, + "name": "randomizerDevice" + }, + { + "enabled": False, + "name": "redshift4maya" + }, + { + "enabled": True, + "name": "renderSetup" + }, + { + "enabled": False, + "name": "retargeterNodes" + }, + { + "enabled": False, + "name": "RokokoMotionLibrary" + }, + { + "enabled": False, + "name": "rotateHelper" + }, + { + "enabled": False, + "name": "sceneAssembly" + }, + { + "enabled": False, + "name": "shaderFXPlugin" + }, + { + "enabled": False, + "name": "shotCamera" + }, + { + "enabled": False, + "name": "snapTransform" + }, + { + "enabled": False, + "name": "stage" + }, + { + "enabled": True, + "name": "stereoCamera" + }, + { + "enabled": False, + "name": "stlTranslator" + }, + { + "enabled": False, + "name": "studioImport" + }, + { + "enabled": False, + "name": "Substance" + }, + { + "enabled": False, + "name": "substancelink" + }, + { + "enabled": False, + "name": "substancemaya" + }, + { + "enabled": False, + "name": "substanceworkflow" + }, + { + "enabled": False, + "name": "svgFileTranslator" + }, + { + "enabled": False, + "name": "sweep" + }, + { + "enabled": False, + "name": "testify" + }, + { + "enabled": False, + "name": "tiffFloatReader" + }, + { + "enabled": False, + "name": "timeSliderBookmark" + }, + { + "enabled": False, + "name": "Turtle" + }, + { + "enabled": False, + "name": "Type" + }, + { + "enabled": False, + "name": "udpDevice" + }, + { + "enabled": False, + "name": "ufeSupport" + }, + { + "enabled": False, + "name": "Unfold3D" + }, + { + "enabled": False, + "name": "VectorRender" + }, + { + "enabled": False, + "name": "vrayformaya" + }, + { + "enabled": False, + "name": "vrayvolumegrid" + }, + { + "enabled": False, + "name": "xgenToolkit" + }, + { + "enabled": False, + "name": "xgenVray" + } + ] +} diff --git a/server_addon/maya/server/settings/imageio.py b/server_addon/maya/server/settings/imageio.py new file mode 100644 index 0000000000..7512bfe253 --- /dev/null +++ b/server_addon/maya/server/settings/imageio.py @@ -0,0 +1,126 @@ +"""Providing models and setting values for image IO in Maya. + +Note: Names were changed to get rid of the versions in class names. +""" +from pydantic import Field, validator + +from ayon_server.settings import BaseSettingsModel, 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 ColorManagementPreferenceV2Model(BaseSettingsModel): + """Color Management Preference v2 (Maya 2022+).""" + _layout = "expanded" + + enabled: bool = Field(True, title="Use Color Management Preference v2") + + renderSpace: str = Field(title="Rendering Space") + displayName: str = Field(title="Display") + viewName: str = Field(title="View") + + +class ColorManagementPreferenceModel(BaseSettingsModel): + """Color Management Preference (legacy).""" + _layout = "expanded" + + renderSpace: str = Field(title="Rendering Space") + viewTransform: str = Field(title="Viewer Transform ") + + +class WorkfileImageIOModel(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + renderSpace: str = Field(title="Rendering Space") + displayName: str = Field(title="Display") + viewName: str = Field(title="View") + + +class ImageIOSettings(BaseSettingsModel): + """Maya color management project settings. + + Todo: What to do with color management preferences version? + """ + + _isGroup: bool = True + 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" + ) + workfile: WorkfileImageIOModel = Field( + default_factory=WorkfileImageIOModel, + title="Workfile" + ) + # Deprecated + colorManagementPreference_v2: ColorManagementPreferenceV2Model = Field( + default_factory=ColorManagementPreferenceV2Model, + title="Color Management Preference v2 (Maya 2022+)" + ) + colorManagementPreference: ColorManagementPreferenceModel = Field( + default_factory=ColorManagementPreferenceModel, + title="Color Management Preference (legacy)" + ) + + +DEFAULT_IMAGEIO_SETTINGS = { + "activate_host_color_management": True, + "ocio_config": { + "override_global_config": False, + "filepath": [] + }, + "file_rules": { + "activate_host_rules": False, + "rules": [] + }, + "workfile": { + "enabled": False, + "renderSpace": "ACES - ACEScg", + "displayName": "ACES", + "viewName": "sRGB" + }, + "colorManagementPreference_v2": { + "enabled": True, + "renderSpace": "ACEScg", + "displayName": "sRGB", + "viewName": "ACES 1.0 SDR-video" + }, + "colorManagementPreference": { + "renderSpace": "scene-linear Rec 709/sRGB", + "viewTransform": "sRGB gamma" + } +} diff --git a/server_addon/maya/server/settings/include_handles.py b/server_addon/maya/server/settings/include_handles.py new file mode 100644 index 0000000000..3ba6aca66b --- /dev/null +++ b/server_addon/maya/server/settings/include_handles.py @@ -0,0 +1,30 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel, task_types_enum + + +class IncludeByTaskTypeModel(BaseSettingsModel): + task_type: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + include_handles: bool = Field(True, title="Include handles") + + +class IncludeHandlesModel(BaseSettingsModel): + """Maya dirmap settings.""" + # _layout = "expanded" + include_handles_default: bool = Field( + True, title="Include handles by default" + ) + per_task_type: list[IncludeByTaskTypeModel] = Field( + default_factory=list, + title="Include/exclude handles by task type" + ) + + +DEFAULT_INCLUDE_HANDLES = { + "include_handles_default": False, + "per_task_type": [] +} diff --git a/server_addon/maya/server/settings/loaders.py b/server_addon/maya/server/settings/loaders.py new file mode 100644 index 0000000000..60fc2a1cdd --- /dev/null +++ b/server_addon/maya/server/settings/loaders.py @@ -0,0 +1,115 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel +from ayon_server.types import ColorRGBA_uint8 + + +class ColorsSetting(BaseSettingsModel): + model: ColorRGBA_uint8 = Field( + (209, 132, 30, 1.0), title="Model:") + rig: ColorRGBA_uint8 = Field( + (59, 226, 235, 1.0), title="Rig:") + pointcache: ColorRGBA_uint8 = Field( + (94, 209, 30, 1.0), title="Pointcache:") + animation: ColorRGBA_uint8 = Field( + (94, 209, 30, 1.0), title="Animation:") + ass: ColorRGBA_uint8 = Field( + (249, 135, 53, 1.0), title="Arnold StandIn:") + camera: ColorRGBA_uint8 = Field( + (136, 114, 244, 1.0), title="Camera:") + fbx: ColorRGBA_uint8 = Field( + (215, 166, 255, 1.0), title="FBX:") + mayaAscii: ColorRGBA_uint8 = Field( + (67, 174, 255, 1.0), title="Maya Ascii:") + mayaScene: ColorRGBA_uint8 = Field( + (67, 174, 255, 1.0), title="Maya Scene:") + setdress: ColorRGBA_uint8 = Field( + (255, 250, 90, 1.0), title="Set Dress:") + layout: ColorRGBA_uint8 = Field(( + 255, 250, 90, 1.0), title="Layout:") + vdbcache: ColorRGBA_uint8 = Field( + (249, 54, 0, 1.0), title="VDB Cache:") + vrayproxy: ColorRGBA_uint8 = Field( + (255, 150, 12, 1.0), title="VRay Proxy:") + vrayscene_layer: ColorRGBA_uint8 = Field( + (255, 150, 12, 1.0), title="VRay Scene:") + yeticache: ColorRGBA_uint8 = Field( + (99, 206, 220, 1.0), title="Yeti Cache:") + yetiRig: ColorRGBA_uint8 = Field( + (0, 205, 125, 1.0), title="Yeti Rig:") + + +class ReferenceLoaderModel(BaseSettingsModel): + namespace: str = Field(title="Namespace") + group_name: str = Field(title="Group name") + display_handle: bool = Field(title="Display Handle On Load References") + + +class LoadersModel(BaseSettingsModel): + colors: ColorsSetting = Field( + default_factory=ColorsSetting, + title="Loaded Products Outliner Colors") + + reference_loader: ReferenceLoaderModel = Field( + default_factory=ReferenceLoaderModel, + title="Reference Loader" + ) + + +DEFAULT_LOADERS_SETTING = { + "colors": { + "model": [ + 209, 132, 30, 1.0 + ], + "rig": [ + 59, 226, 235, 1.0 + ], + "pointcache": [ + 94, 209, 30, 1.0 + ], + "animation": [ + 94, 209, 30, 1.0 + ], + "ass": [ + 249, 135, 53, 1.0 + ], + "camera": [ + 136, 114, 244, 1.0 + ], + "fbx": [ + 215, 166, 255, 1.0 + ], + "mayaAscii": [ + 67, 174, 255, 1.0 + ], + "mayaScene": [ + 67, 174, 255, 1.0 + ], + "setdress": [ + 255, 250, 90, 1.0 + ], + "layout": [ + 255, 250, 90, 1.0 + ], + "vdbcache": [ + 249, 54, 0, 1.0 + ], + "vrayproxy": [ + 255, 150, 12, 1.0 + ], + "vrayscene_layer": [ + 255, 150, 12, 1.0 + ], + "yeticache": [ + 99, 206, 220, 1.0 + ], + "yetiRig": [ + 0, 205, 125, 1.0 + ] + }, + "reference_loader": { + "namespace": "{folder[name]}_{product[name]}_##_", + "group_name": "_GRP", + "display_handle": True + } +} diff --git a/server_addon/maya/server/settings/main.py b/server_addon/maya/server/settings/main.py new file mode 100644 index 0000000000..47f4121584 --- /dev/null +++ b/server_addon/maya/server/settings/main.py @@ -0,0 +1,139 @@ +from pydantic import Field, validator +from ayon_server.settings import BaseSettingsModel, ensure_unique_names +from .imageio import ImageIOSettings, DEFAULT_IMAGEIO_SETTINGS +from .maya_dirmap import MayaDirmapModel, DEFAULT_MAYA_DIRMAP_SETTINGS +from .include_handles import IncludeHandlesModel, DEFAULT_INCLUDE_HANDLES +from .explicit_plugins_loading import ( + ExplicitPluginsLoadingModel, DEFAULT_EXPLITCIT_PLUGINS_LOADING_SETTINGS +) +from .scriptsmenu import ScriptsmenuModel, DEFAULT_SCRIPTSMENU_SETTINGS +from .render_settings import RenderSettingsModel, DEFAULT_RENDER_SETTINGS +from .creators import CreatorsModel, DEFAULT_CREATORS_SETTINGS +from .publishers import PublishersModel, DEFAULT_PUBLISH_SETTINGS +from .loaders import LoadersModel, DEFAULT_LOADERS_SETTING +from .workfile_build_settings import ProfilesModel, DEFAULT_WORKFILE_SETTING +from .templated_workfile_settings import ( + TemplatedProfilesModel, DEFAULT_TEMPLATED_WORKFILE_SETTINGS +) + + +class ExtMappingItemModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Product type") + value: str = Field(title="Extension") + + +class PublishGUIFilterItemModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: bool = Field(True, title="Active") + + +class PublishGUIFiltersModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: list[PublishGUIFilterItemModel] = Field(default_factory=list) + + @validator("value") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class MayaSettings(BaseSettingsModel): + """Maya Project Settings.""" + + open_workfile_post_initialization: bool = Field( + True, title="Open Workfile Post Initialization") + explicit_plugins_loading: ExplicitPluginsLoadingModel = Field( + default_factory=ExplicitPluginsLoadingModel, + title="Explicit Plugins Loading") + imageio: ImageIOSettings = Field( + default_factory=ImageIOSettings, title="Color Management (imageio)") + mel_workspace: str = Field(title="Maya MEL Workspace", widget="textarea") + ext_mapping: list[ExtMappingItemModel] = Field( + default_factory=list, title="Extension Mapping") + maya_dirmap: MayaDirmapModel = Field( + default_factory=MayaDirmapModel, title="Maya dirmap Settings") + include_handles: IncludeHandlesModel = Field( + default_factory=IncludeHandlesModel, + title="Include/Exclude Handles in default playback & render range" + ) + scriptsmenu: ScriptsmenuModel = Field( + default_factory=ScriptsmenuModel, title="Scriptsmenu Settings") + render_settings: RenderSettingsModel = Field( + default_factory=RenderSettingsModel, title="Render Settings") + create: CreatorsModel = Field( + default_factory=CreatorsModel, title="Creators") + publish: PublishersModel = Field( + default_factory=PublishersModel, title="Publishers") + load: LoadersModel = Field( + default_factory=LoadersModel, title="Loaders") + workfile_build: ProfilesModel = Field( + default_factory=ProfilesModel, title="Workfile Build Settings") + templated_workfile_build: TemplatedProfilesModel = Field( + default_factory=TemplatedProfilesModel, + title="Templated Workfile Build Settings") + filters: list[PublishGUIFiltersModel] = Field( + default_factory=list, + title="Publish GUI Filters") + + @validator("filters", "ext_mapping") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +DEFAULT_MEL_WORKSPACE_SETTINGS = "\n".join(( + 'workspace -fr "shaders" "renderData/shaders";', + 'workspace -fr "images" "renders/maya";', + 'workspace -fr "particles" "particles";', + 'workspace -fr "mayaAscii" "";', + 'workspace -fr "mayaBinary" "";', + 'workspace -fr "scene" "";', + 'workspace -fr "alembicCache" "cache/alembic";', + 'workspace -fr "renderData" "renderData";', + 'workspace -fr "sourceImages" "sourceimages";', + 'workspace -fr "fileCache" "cache/nCache";', + '', +)) + +DEFAULT_MAYA_SETTING = { + "open_workfile_post_initialization": False, + "explicit_plugins_loading": DEFAULT_EXPLITCIT_PLUGINS_LOADING_SETTINGS, + "imageio": DEFAULT_IMAGEIO_SETTINGS, + "mel_workspace": DEFAULT_MEL_WORKSPACE_SETTINGS, + "ext_mapping": [ + {"name": "model", "value": "ma"}, + {"name": "mayaAscii", "value": "ma"}, + {"name": "camera", "value": "ma"}, + {"name": "rig", "value": "ma"}, + {"name": "workfile", "value": "ma"}, + {"name": "yetiRig", "value": "ma"} + ], + # `maya_dirmap` was originally with dash - `maya-dirmap` + "maya_dirmap": DEFAULT_MAYA_DIRMAP_SETTINGS, + "include_handles": DEFAULT_INCLUDE_HANDLES, + "scriptsmenu": DEFAULT_SCRIPTSMENU_SETTINGS, + "render_settings": DEFAULT_RENDER_SETTINGS, + "create": DEFAULT_CREATORS_SETTINGS, + "publish": DEFAULT_PUBLISH_SETTINGS, + "load": DEFAULT_LOADERS_SETTING, + "workfile_build": DEFAULT_WORKFILE_SETTING, + "templated_workfile_build": DEFAULT_TEMPLATED_WORKFILE_SETTINGS, + "filters": [ + { + "name": "preset 1", + "value": [ + {"name": "ValidateNoAnimation", "value": False}, + {"name": "ValidateShapeDefaultNames", "value": False}, + ] + }, + { + "name": "preset 2", + "value": [ + {"name": "ValidateNoAnimation", "value": False}, + ] + }, + ] +} diff --git a/server_addon/maya/server/settings/maya_dirmap.py b/server_addon/maya/server/settings/maya_dirmap.py new file mode 100644 index 0000000000..243261dc87 --- /dev/null +++ b/server_addon/maya/server/settings/maya_dirmap.py @@ -0,0 +1,40 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class MayaDirmapPathsSubmodel(BaseSettingsModel): + _layout = "compact" + source_path: list[str] = Field( + default_factory=list, title="Source Paths" + ) + destination_path: list[str] = Field( + default_factory=list, title="Destination Paths" + ) + + +class MayaDirmapModel(BaseSettingsModel): + """Maya dirmap settings.""" + # _layout = "expanded" + _isGroup: bool = True + + enabled: bool = Field(title="enabled") + # Use ${} placeholder instead of absolute value of a root in + # referenced filepaths. + use_env_var_as_root: bool = Field( + title="Use env var placeholder in referenced paths" + ) + paths: MayaDirmapPathsSubmodel = Field( + default_factory=MayaDirmapPathsSubmodel, + title="Dirmap Paths" + ) + + +DEFAULT_MAYA_DIRMAP_SETTINGS = { + "use_env_var_as_root": False, + "enabled": False, + "paths": { + "source-path": [], + "destination-path": [] + } +} diff --git a/server_addon/maya/server/settings/publish_playblast.py b/server_addon/maya/server/settings/publish_playblast.py new file mode 100644 index 0000000000..acfcaf5988 --- /dev/null +++ b/server_addon/maya/server/settings/publish_playblast.py @@ -0,0 +1,382 @@ +from pydantic import Field, validator + +from ayon_server.settings import ( + BaseSettingsModel, + ensure_unique_names, + task_types_enum, +) +from ayon_server.types import ColorRGBA_uint8 + + +def hardware_falloff_enum(): + return [ + {"label": "Linear", "value": "0"}, + {"label": "Exponential", "value": "1"}, + {"label": "Exponential Squared", "value": "2"} + ] + + +def renderer_enum(): + return [ + {"label": "Viewport 2.0", "value": "vp2Renderer"} + ] + + +def displayLights_enum(): + return [ + {"label": "Default Lighting", "value": "default"}, + {"label": "All Lights", "value": "all"}, + {"label": "Selected Lights", "value": "selected"}, + {"label": "Flat Lighting", "value": "flat"}, + {"label": "No Lights", "value": "nolights"} + ] + + +def plugin_objects_default(): + return [ + { + "name": "gpuCacheDisplayFilter", + "value": False + } + ] + + +class CodecSetting(BaseSettingsModel): + _layout = "expanded" + compression: str = Field("png", title="Encoding") + format: str = Field("image", title="Format") + quality: int = Field(95, title="Quality", ge=0, le=100) + + +class DisplayOptionsSetting(BaseSettingsModel): + _layout = "expanded" + override_display: bool = Field(True, title="Override display options") + background: ColorRGBA_uint8 = Field( + (125, 125, 125, 1.0), title="Background Color" + ) + displayGradient: bool = Field(True, title="Display background gradient") + backgroundTop: ColorRGBA_uint8 = Field( + (125, 125, 125, 1.0), title="Background Top" + ) + backgroundBottom: ColorRGBA_uint8 = Field( + (125, 125, 125, 1.0), title="Background Bottom" + ) + + +class GenericSetting(BaseSettingsModel): + _layout = "expanded" + isolate_view: bool = Field(True, title="Isolate View") + off_screen: bool = Field(True, title="Off Screen") + pan_zoom: bool = Field(False, title="2D Pan/Zoom") + + +class RendererSetting(BaseSettingsModel): + _layout = "expanded" + rendererName: str = Field( + "vp2Renderer", + enum_resolver=renderer_enum, + title="Renderer name" + ) + + +class ResolutionSetting(BaseSettingsModel): + _layout = "expanded" + width: int = Field(0, title="Width") + height: int = Field(0, title="Height") + + +class PluginObjectsModel(BaseSettingsModel): + name: str = Field("", title="Name") + value: bool = Field(True, title="Enabled") + + +class ViewportOptionsSetting(BaseSettingsModel): + override_viewport_options: bool = Field( + True, title="Override viewport options" + ) + displayLights: str = Field( + "default", enum_resolver=displayLights_enum, title="Display Lights" + ) + displayTextures: bool = Field(True, title="Display Textures") + textureMaxResolution: int = Field(1024, title="Texture Clamp Resolution") + renderDepthOfField: bool = Field( + True, title="Depth of Field", section="Depth of Field" + ) + shadows: bool = Field(True, title="Display Shadows") + twoSidedLighting: bool = Field(True, title="Two Sided Lighting") + lineAAEnable: bool = Field( + True, title="Enable Anti-Aliasing", section="Anti-Aliasing" + ) + multiSample: int = Field(8, title="Anti Aliasing Samples") + useDefaultMaterial: bool = Field(False, title="Use Default Material") + wireframeOnShaded: bool = Field(False, title="Wireframe On Shaded") + xray: bool = Field(False, title="X-Ray") + jointXray: bool = Field(False, title="X-Ray Joints") + backfaceCulling: bool = Field(False, title="Backface Culling") + ssaoEnable: bool = Field( + False, title="Screen Space Ambient Occlusion", section="SSAO" + ) + ssaoAmount: int = Field(1, title="SSAO Amount") + ssaoRadius: int = Field(16, title="SSAO Radius") + ssaoFilterRadius: int = Field(16, title="SSAO Filter Radius") + ssaoSamples: int = Field(16, title="SSAO Samples") + fogging: bool = Field(False, title="Enable Hardware Fog", section="Fog") + hwFogFalloff: str = Field( + "0", enum_resolver=hardware_falloff_enum, title="Hardware Falloff" + ) + hwFogDensity: float = Field(0.0, title="Fog Density") + hwFogStart: int = Field(0, title="Fog Start") + hwFogEnd: int = Field(100, title="Fog End") + hwFogAlpha: int = Field(0, title="Fog Alpha") + hwFogColorR: float = Field(1.0, title="Fog Color R") + hwFogColorG: float = Field(1.0, title="Fog Color G") + hwFogColorB: float = Field(1.0, title="Fog Color B") + motionBlurEnable: bool = Field( + False, title="Enable Motion Blur", section="Motion Blur" + ) + motionBlurSampleCount: int = Field(8, title="Motion Blur Sample Count") + motionBlurShutterOpenFraction: float = Field( + 0.2, title="Shutter Open Fraction" + ) + cameras: bool = Field(False, title="Cameras", section="Show") + clipGhosts: bool = Field(False, title="Clip Ghosts") + deformers: bool = Field(False, title="Deformers") + dimensions: bool = Field(False, title="Dimensions") + dynamicConstraints: bool = Field(False, title="Dynamic Constraints") + dynamics: bool = Field(False, title="Dynamics") + fluids: bool = Field(False, title="Fluids") + follicles: bool = Field(False, title="Follicles") + greasePencils: bool = Field(False, title="Grease Pencils") + grid: bool = Field(False, title="Grid") + hairSystems: bool = Field(True, title="Hair Systems") + handles: bool = Field(False, title="Handles") + headsUpDisplay: bool = Field(False, title="HUD") + ikHandles: bool = Field(False, title="IK Handles") + imagePlane: bool = Field(True, title="Image Plane") + joints: bool = Field(False, title="Joints") + lights: bool = Field(False, title="Lights") + locators: bool = Field(False, title="Locators") + manipulators: bool = Field(False, title="Manipulators") + motionTrails: bool = Field(False, title="Motion Trails") + nCloths: bool = Field(False, title="nCloths") + nParticles: bool = Field(False, title="nParticles") + nRigids: bool = Field(False, title="nRigids") + controlVertices: bool = Field(False, title="NURBS CVs") + nurbsCurves: bool = Field(False, title="NURBS Curves") + hulls: bool = Field(False, title="NURBS Hulls") + nurbsSurfaces: bool = Field(False, title="NURBS Surfaces") + particleInstancers: bool = Field(False, title="Particle Instancers") + pivots: bool = Field(False, title="Pivots") + planes: bool = Field(False, title="Planes") + pluginShapes: bool = Field(False, title="Plugin Shapes") + polymeshes: bool = Field(True, title="Polygons") + strokes: bool = Field(False, title="Strokes") + subdivSurfaces: bool = Field(False, title="Subdiv Surfaces") + textures: bool = Field(False, title="Texture Placements") + pluginObjects: list[PluginObjectsModel] = Field( + default_factory=plugin_objects_default, + title="Plugin Objects" + ) + + @validator("pluginObjects") + def validate_unique_plugin_objects(cls, value): + ensure_unique_names(value) + return value + + +class CameraOptionsSetting(BaseSettingsModel): + displayGateMask: bool = Field(False, title="Display Gate Mask") + displayResolution: bool = Field(False, title="Display Resolution") + displayFilmGate: bool = Field(False, title="Display Film Gate") + displayFieldChart: bool = Field(False, title="Display Field Chart") + displaySafeAction: bool = Field(False, title="Display Safe Action") + displaySafeTitle: bool = Field(False, title="Display Safe Title") + displayFilmPivot: bool = Field(False, title="Display Film Pivot") + displayFilmOrigin: bool = Field(False, title="Display Film Origin") + overscan: int = Field(1.0, title="Overscan") + + +class CapturePresetSetting(BaseSettingsModel): + Codec: CodecSetting = Field( + default_factory=CodecSetting, + title="Codec", + section="Codec") + DisplayOptions: DisplayOptionsSetting = Field( + default_factory=DisplayOptionsSetting, + title="Display Options", + section="Display Options") + Generic: GenericSetting = Field( + default_factory=GenericSetting, + title="Generic", + section="Generic") + Renderer: RendererSetting = Field( + default_factory=RendererSetting, + title="Renderer", + section="Renderer") + Resolution: ResolutionSetting = Field( + default_factory=ResolutionSetting, + title="Resolution", + section="Resolution") + ViewportOptions: ViewportOptionsSetting = Field( + default_factory=ViewportOptionsSetting, + title="Viewport Options") + CameraOptions: CameraOptionsSetting = Field( + default_factory=CameraOptionsSetting, + title="Camera Options") + + +class ProfilesModel(BaseSettingsModel): + _layout = "expanded" + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field(default_factory=list, title="Task names") + product_names: list[str] = Field(default_factory=list, title="Products names") + capture_preset: CapturePresetSetting = Field( + default_factory=CapturePresetSetting, + title="Capture Preset" + ) + + +class ExtractPlayblastSetting(BaseSettingsModel): + capture_preset: CapturePresetSetting = Field( + default_factory=CapturePresetSetting, + title="DEPRECATED! Please use \"Profiles\" below. Capture Preset" + ) + profiles: list[ProfilesModel] = Field( + default_factory=list, + title="Profiles" + ) + + +DEFAULT_PLAYBLAST_SETTING = { + "capture_preset": { + "Codec": { + "compression": "png", + "format": "image", + "quality": 95 + }, + "DisplayOptions": { + "override_display": True, + "background": [ + 125, + 125, + 125, + 1.0 + ], + "backgroundBottom": [ + 125, + 125, + 125, + 1.0 + ], + "backgroundTop": [ + 125, + 125, + 125, + 1.0 + ], + "displayGradient": True + }, + "Generic": { + "isolate_view": True, + "off_screen": True, + "pan_zoom": False + }, + "Renderer": { + "rendererName": "vp2Renderer" + }, + "Resolution": { + "width": 1920, + "height": 1080 + }, + "ViewportOptions": { + "override_viewport_options": True, + "displayLights": "default", + "displayTextures": True, + "textureMaxResolution": 1024, + "renderDepthOfField": True, + "shadows": True, + "twoSidedLighting": True, + "lineAAEnable": True, + "multiSample": 8, + "useDefaultMaterial": False, + "wireframeOnShaded": False, + "xray": False, + "jointXray": False, + "backfaceCulling": False, + "ssaoEnable": False, + "ssaoAmount": 1, + "ssaoRadius": 16, + "ssaoFilterRadius": 16, + "ssaoSamples": 16, + "fogging": False, + "hwFogFalloff": "0", + "hwFogDensity": 0.0, + "hwFogStart": 0, + "hwFogEnd": 100, + "hwFogAlpha": 0, + "hwFogColorR": 1.0, + "hwFogColorG": 1.0, + "hwFogColorB": 1.0, + "motionBlurEnable": False, + "motionBlurSampleCount": 8, + "motionBlurShutterOpenFraction": 0.2, + "cameras": False, + "clipGhosts": False, + "deformers": False, + "dimensions": False, + "dynamicConstraints": False, + "dynamics": False, + "fluids": False, + "follicles": False, + "greasePencils": False, + "grid": False, + "hairSystems": True, + "handles": False, + "headsUpDisplay": False, + "ikHandles": False, + "imagePlane": True, + "joints": False, + "lights": False, + "locators": False, + "manipulators": False, + "motionTrails": False, + "nCloths": False, + "nParticles": False, + "nRigids": False, + "controlVertices": False, + "nurbsCurves": False, + "hulls": False, + "nurbsSurfaces": False, + "particleInstancers": False, + "pivots": False, + "planes": False, + "pluginShapes": False, + "polymeshes": True, + "strokes": False, + "subdivSurfaces": False, + "textures": False, + "pluginObjects": [ + { + "name": "gpuCacheDisplayFilter", + "value": False + } + ] + }, + "CameraOptions": { + "displayGateMask": False, + "displayResolution": False, + "displayFilmGate": False, + "displayFieldChart": False, + "displaySafeAction": False, + "displaySafeTitle": False, + "displayFilmPivot": False, + "displayFilmOrigin": False, + "overscan": 1.0 + } + }, + "profiles": [] +} diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py new file mode 100644 index 0000000000..bd7ccdf4d5 --- /dev/null +++ b/server_addon/maya/server/settings/publishers.py @@ -0,0 +1,1262 @@ +import json +from pydantic import Field, validator +from ayon_server.settings import ( + BaseSettingsModel, + MultiplatformPathModel, + ensure_unique_names, +) +from ayon_server.exceptions import BadRequestException +from .publish_playblast import ( + ExtractPlayblastSetting, + DEFAULT_PLAYBLAST_SETTING, +) + + +def linear_unit_enum(): + """Get linear units enumerator.""" + return [ + {"label": "mm", "value": "millimeter"}, + {"label": "cm", "value": "centimeter"}, + {"label": "m", "value": "meter"}, + {"label": "km", "value": "kilometer"}, + {"label": "in", "value": "inch"}, + {"label": "ft", "value": "foot"}, + {"label": "yd", "value": "yard"}, + {"label": "mi", "value": "mile"} + ] + + +def angular_unit_enum(): + """Get angular units enumerator.""" + return [ + {"label": "deg", "value": "degree"}, + {"label": "rad", "value": "radian"}, + ] + + +class BasicValidateModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + + +class ValidateMeshUVSetMap1Model(BasicValidateModel): + """Validate model's default uv set exists and is named 'map1'.""" + pass + + +class ValidateNoAnimationModel(BasicValidateModel): + """Ensure no keyframes on nodes in the Instance.""" + pass + + +class ValidateRigOutSetNodeIdsModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateSkinclusterDeformerSet") + optional: bool = Field(title="Optional") + allow_history_only: bool = Field(title="Allow history only") + + +class ValidateModelNameModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + database: bool = Field(title="Use database shader name definitions") + material_file: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Material File", + description=( + "Path to material file defining list of material names to check." + ) + ) + regex: str = Field( + "(.*)_(\\d)*_(?P.*)_(GEO)", + title="Validation regex", + description=( + "Regex for validating name of top level group name. You can use" + " named capturing groups:(?P.*) for Asset name" + ) + ) + top_level_regex: str = Field( + ".*_GRP", + title="Top level group name regex", + description=( + "To check for asset in name so *_some_asset_name_GRP" + " is valid, use:.*?_(?P.*)_GEO" + ) + ) + + +class ValidateModelContentModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + validate_top_group: bool = Field(title="Validate one top group") + + +class ValidateTransformNamingSuffixModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + SUFFIX_NAMING_TABLE: str = Field( + "{}", + title="Suffix Naming Tables", + widget="textarea", + description=( + "Validates transform suffix based on" + " the type of its children shapes." + ) + ) + + @validator("SUFFIX_NAMING_TABLE") + def validate_json(cls, value): + if not value.strip(): + return "{}" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, dict) + except json.JSONDecodeError: + success = False + + if not success: + raise BadRequestException( + "The text can't be parsed as json object" + ) + return value + ALLOW_IF_NOT_IN_SUFFIX_TABLE: bool = Field( + title="Allow if suffix not in table" + ) + + +class CollectMayaRenderModel(BaseSettingsModel): + sync_workfile_version: bool = Field( + title="Sync render version with workfile" + ) + + +class CollectFbxCameraModel(BaseSettingsModel): + enabled: bool = Field(title="CollectFbxCamera") + + +class CollectGLTFModel(BaseSettingsModel): + enabled: bool = Field(title="CollectGLTF") + + +class ValidateFrameRangeModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateFrameRange") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + exclude_product_types: list[str] = Field( + default_factory=list, + title="Exclude product types" + ) + + +class ValidateShaderNameModel(BaseSettingsModel): + """ + Shader name regex can use named capture group asset to validate against current asset name. + """ + enabled: bool = Field(title="ValidateShaderName") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + regex: str = Field("(?P.*)_(.*)_SHD", title="Validation regex") + + +class ValidateAttributesModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateAttributes") + attributes: str = Field( + "{}", title="Attributes", widget="textarea") + + @validator("attributes") + def validate_json(cls, value): + if not value.strip(): + return "{}" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, dict) + except json.JSONDecodeError: + success = False + + if not success: + raise BadRequestException( + "The attibutes can't be parsed as json object" + ) + return value + + +class ValidateLoadedPluginModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateLoadedPlugin") + optional: bool = Field(title="Optional") + whitelist_native_plugins: bool = Field( + title="Whitelist Maya Native Plugins" + ) + authorized_plugins: list[str] = Field( + default_factory=list, title="Authorized plugins" + ) + + +class ValidateMayaUnitsModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateMayaUnits") + optional: bool = Field(title="Optional") + validate_linear_units: bool = Field(title="Validate linear units") + linear_units: str = Field( + enum_resolver=linear_unit_enum, title="Linear Units" + ) + validate_angular_units: bool = Field(title="Validate angular units") + angular_units: str = Field( + enum_resolver=angular_unit_enum, title="Angular units" + ) + validate_fps: bool = Field(title="Validate fps") + + +class ValidateUnrealStaticMeshNameModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateUnrealStaticMeshName") + optional: bool = Field(title="Optional") + validate_mesh: bool = Field(title="Validate mesh names") + validate_collision: bool = Field(title="Validate collison names") + + +class ValidateCycleErrorModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateCycleError") + optional: bool = Field(title="Optional") + families: list[str] = Field(default_factory=list, title="Families") + + +class ValidatePluginPathAttributesAttrModel(BaseSettingsModel): + name: str = Field(title="Node type") + value: str = Field(title="Attribute") + + +class ValidatePluginPathAttributesModel(BaseSettingsModel): + """Fill in the node types and attributes you want to validate. + +

e.g. AlembicNode.abc_file, the node type is AlembicNode + and the node attribute is abc_file + """ + + enabled: bool = True + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + attribute: list[ValidatePluginPathAttributesAttrModel] = Field( + default_factory=list, + title="File Attribute" + ) + + @validator("attribute") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +# Validate Render Setting +class RendererAttributesModel(BaseSettingsModel): + _layout = "compact" + type: str = Field(title="Type") + value: str = Field(title="Value") + + +class ValidateRenderSettingsModel(BaseSettingsModel): + arnold_render_attributes: list[RendererAttributesModel] = Field( + default_factory=list, title="Arnold Render Attributes") + vray_render_attributes: list[RendererAttributesModel] = Field( + default_factory=list, title="VRay Render Attributes") + redshift_render_attributes: list[RendererAttributesModel] = Field( + default_factory=list, title="Redshift Render Attributes") + renderman_render_attributes: list[RendererAttributesModel] = Field( + default_factory=list, title="Renderman Render Attributes") + + +class BasicValidateModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + + +class ValidateCameraContentsModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + validate_shapes: bool = Field(title="Validate presence of shapes") + + +class ExtractProxyAlembicModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + families: list[str] = Field( + default_factory=list, + title="Families") + + +class ExtractAlembicModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + families: list[str] = Field( + default_factory=list, + title="Families") + + +class ExtractObjModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + + +class ExtractMayaSceneRawModel(BaseSettingsModel): + """Add loaded instances to those published families:""" + enabled: bool = Field(title="ExtractMayaSceneRaw") + add_for_families: list[str] = Field(default_factory=list, title="Families") + + +class ExtractCameraAlembicModel(BaseSettingsModel): + """ + List of attributes that will be added to the baked alembic camera. Needs to be written in python list syntax. + """ + enabled: bool = Field(title="ExtractCameraAlembic") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + bake_attributes: str = Field( + "[]", title="Base Attributes", widget="textarea" + ) + + @validator("bake_attributes") + def validate_json_list(cls, value): + if not value.strip(): + return "[]" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, list) + except json.JSONDecodeError: + success = False + + if not success: + raise BadRequestException( + "The text can't be parsed as json object" + ) + return value + + +class ExtractGLBModel(BaseSettingsModel): + enabled: bool = True + active: bool = Field(title="Active") + ogsfx_path: str = Field(title="GLSL Shader Directory") + + +class ExtractLookArgsModel(BaseSettingsModel): + argument: str = Field(title="Argument") + parameters: list[str] = Field(default_factory=list, title="Parameters") + + +class ExtractLookModel(BaseSettingsModel): + maketx_arguments: list[ExtractLookArgsModel] = Field( + default_factory=list, + title="Extra arguments for maketx command line" + ) + + +class ExtractGPUCacheModel(BaseSettingsModel): + enabled: bool = True + families: list[str] = Field(default_factory=list, title="Families") + step: float = Field(1.0, ge=1.0, title="Step") + stepSave: int = Field(1, ge=1, title="Step Save") + optimize: bool = Field(title="Optimize Hierarchy") + optimizationThreshold: int = Field(1, ge=1, title="Optimization Threshold") + optimizeAnimationsForMotionBlur: bool = Field( + title="Optimize Animations For Motion Blur" + ) + writeMaterials: bool = Field(title="Write Materials") + useBaseTessellation: bool = Field(title="User Base Tesselation") + + +class PublishersModel(BaseSettingsModel): + CollectMayaRender: CollectMayaRenderModel = Field( + default_factory=CollectMayaRenderModel, + title="Collect Render Layers", + section="Collectors" + ) + CollectFbxCamera: CollectFbxCameraModel = Field( + default_factory=CollectFbxCameraModel, + title="Collect Camera for FBX export", + ) + CollectGLTF: CollectGLTFModel = Field( + default_factory=CollectGLTFModel, + title="Collect Assets for GLB/GLTF export" + ) + ValidateInstanceInContext: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Instance In Context", + section="Validators" + ) + ValidateContainers: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Containers" + ) + ValidateFrameRange: ValidateFrameRangeModel = Field( + default_factory=ValidateFrameRangeModel, + title="Validate Frame Range" + ) + ValidateShaderName: ValidateShaderNameModel = Field( + default_factory=ValidateShaderNameModel, + title="Validate Shader Name" + ) + ValidateShadingEngine: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Look Shading Engine Naming" + ) + ValidateMayaColorSpace: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Colorspace" + ) + ValidateAttributes: ValidateAttributesModel = Field( + default_factory=ValidateAttributesModel, + title="Validate Attributes" + ) + ValidateLoadedPlugin: ValidateLoadedPluginModel = Field( + default_factory=ValidateLoadedPluginModel, + title="Validate Loaded Plugin" + ) + ValidateMayaUnits: ValidateMayaUnitsModel = Field( + default_factory=ValidateMayaUnitsModel, + title="Validate Maya Units" + ) + ValidateUnrealStaticMeshName: ValidateUnrealStaticMeshNameModel = Field( + default_factory=ValidateUnrealStaticMeshNameModel, + title="Validate Unreal Static Mesh Name" + ) + ValidateCycleError: ValidateCycleErrorModel = Field( + default_factory=ValidateCycleErrorModel, + title="Validate Cycle Error" + ) + ValidatePluginPathAttributes: ValidatePluginPathAttributesModel = Field( + default_factory=ValidatePluginPathAttributesModel, + title="Plug-in Path Attributes" + ) + ValidateRenderSettings: ValidateRenderSettingsModel = Field( + default_factory=ValidateRenderSettingsModel, + title="Validate Render Settings" + ) + ValidateCurrentRenderLayerIsRenderable: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Current Render Layer Has Renderable Camera" + ) + ValidateGLSLMaterial: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate GLSL Material" + ) + ValidateGLSLPlugin: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate GLSL Plugin" + ) + ValidateRenderImageRule: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Render Image Rule (Workspace)" + ) + ValidateRenderNoDefaultCameras: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate No Default Cameras Renderable" + ) + ValidateRenderSingleCamera: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Render Single Camera " + ) + ValidateRenderLayerAOVs: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Render Passes/AOVs Are Registered" + ) + ValidateStepSize: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Step Size" + ) + ValidateVRayDistributedRendering: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="VRay Distributed Rendering" + ) + ValidateVrayReferencedAOVs: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="VRay Referenced AOVs" + ) + ValidateVRayTranslatorEnabled: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="VRay Translator Settings" + ) + ValidateVrayProxy: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="VRay Proxy Settings" + ) + ValidateVrayProxyMembers: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="VRay Proxy Members" + ) + ValidateYetiRenderScriptCallbacks: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Yeti Render Script Callbacks" + ) + ValidateYetiRigCacheState: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Yeti Rig Cache State" + ) + ValidateYetiRigInputShapesInInstance: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Yeti Rig Input Shapes In Instance" + ) + ValidateYetiRigSettings: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Yeti Rig Settings" + ) + # Model - START + ValidateModelName: ValidateModelNameModel = Field( + default_factory=ValidateModelNameModel, + title="Validate Model Name", + section="Model", + ) + ValidateModelContent: ValidateModelContentModel = Field( + default_factory=ValidateModelContentModel, + title="Validate Model Content", + ) + ValidateTransformNamingSuffix: ValidateTransformNamingSuffixModel = Field( + default_factory=ValidateTransformNamingSuffixModel, + title="Validate Transform Naming Suffix", + ) + ValidateColorSets: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Color Sets", + ) + ValidateMeshHasOverlappingUVs: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Has Overlapping UVs", + ) + ValidateMeshArnoldAttributes: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Arnold Attributes", + ) + ValidateMeshShaderConnections: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Shader Connections", + ) + ValidateMeshSingleUVSet: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Single UV Set", + ) + ValidateMeshHasUVs: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Has UVs", + ) + ValidateMeshLaminaFaces: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Lamina Faces", + ) + ValidateMeshNgons: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Ngons", + ) + ValidateMeshNonManifold: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Non-Manifold", + ) + ValidateMeshNoNegativeScale: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh No Negative Scale", + ) + ValidateMeshNonZeroEdgeLength: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Edge Length Non Zero", + ) + ValidateMeshNormalsUnlocked: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Normals Unlocked", + ) + ValidateMeshUVSetMap1: ValidateMeshUVSetMap1Model = Field( + default_factory=ValidateMeshUVSetMap1Model, + title="Validate Mesh UV Set Map 1", + ) + ValidateMeshVerticesHaveEdges: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh Vertices Have Edges", + ) + ValidateNoAnimation: ValidateNoAnimationModel = Field( + default_factory=ValidateNoAnimationModel, + title="Validate No Animation", + ) + ValidateNoNamespace: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate No Namespace", + ) + ValidateNoNullTransforms: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate No Null Transforms", + ) + ValidateNoUnknownNodes: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate No Unknown Nodes", + ) + ValidateNodeNoGhosting: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Node No Ghosting", + ) + ValidateShapeDefaultNames: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Shape Default Names", + ) + ValidateShapeRenderStats: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Shape Render Stats", + ) + ValidateShapeZero: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Shape Zero", + ) + ValidateTransformZero: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Transform Zero", + ) + ValidateUniqueNames: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Unique Names", + ) + ValidateNoVRayMesh: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate No V-Ray Proxies (VRayMesh)", + ) + ValidateUnrealMeshTriangulated: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate if Mesh is Triangulated", + ) + ValidateAlembicVisibleOnly: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Alembic Visible Node", + ) + ExtractProxyAlembic: ExtractProxyAlembicModel = Field( + default_factory=ExtractProxyAlembicModel, + title="Extract Proxy Alembic", + section="Model Extractors", + ) + ExtractAlembic: ExtractAlembicModel = Field( + default_factory=ExtractAlembicModel, + title="Extract Alembic", + ) + ExtractObj: ExtractObjModel = Field( + default_factory=ExtractObjModel, + title="Extract OBJ" + ) + # Model - END + + # Rig - START + ValidateRigContents: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Rig Contents", + section="Rig", + ) + ValidateRigJointsHidden: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Rig Joints Hidden", + ) + ValidateRigControllers: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Rig Controllers", + ) + ValidateAnimationContent: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Animation Content", + ) + ValidateOutRelatedNodeIds: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Animation Out Set Related Node Ids", + ) + ValidateRigControllersArnoldAttributes: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Rig Controllers (Arnold Attributes)", + ) + ValidateSkeletalMeshHierarchy: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Skeletal Mesh Top Node", + ) + ValidateSkinclusterDeformerSet: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Skincluster Deformer Relationships", + ) + ValidateRigOutSetNodeIds: ValidateRigOutSetNodeIdsModel = Field( + default_factory=ValidateRigOutSetNodeIdsModel, + title="Validate Rig Out Set Node Ids", + ) + # Rig - END + ValidateCameraAttributes: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Camera Attributes" + ) + ValidateAssemblyName: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Assembly Name" + ) + ValidateAssemblyNamespaces: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Assembly Namespaces" + ) + ValidateAssemblyModelTransforms: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Assembly Model Transforms" + ) + ValidateAssRelativePaths: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Ass Relative Paths" + ) + ValidateInstancerContent: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Instancer Content" + ) + ValidateInstancerFrameRanges: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Instancer Cache Frame Ranges" + ) + ValidateNoDefaultCameras: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate No Default Cameras" + ) + ValidateUnrealUpAxis: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Unreal Up-Axis Check" + ) + ValidateCameraContents: ValidateCameraContentsModel = Field( + default_factory=ValidateCameraContentsModel, + title="Validate Camera Content" + ) + ExtractPlayblast: ExtractPlayblastSetting = Field( + default_factory=ExtractPlayblastSetting, + title="Extract Playblast Settings", + section="Extractors" + ) + ExtractMayaSceneRaw: ExtractMayaSceneRawModel = Field( + default_factory=ExtractMayaSceneRawModel, + title="Maya Scene(Raw)" + ) + ExtractCameraAlembic: ExtractCameraAlembicModel = Field( + default_factory=ExtractCameraAlembicModel, + title="Extract Camera Alembic" + ) + ExtractGLB: ExtractGLBModel = Field( + default_factory=ExtractGLBModel, + title="Extract GLB" + ) + ExtractLook: ExtractLookModel = Field( + default_factory=ExtractLookModel, + title="Extract Look" + ) + ExtractGPUCache: ExtractGPUCacheModel = Field( + default_factory=ExtractGPUCacheModel, + title="Extract GPU Cache", + ) + + +DEFAULT_SUFFIX_NAMING = { + "mesh": ["_GEO", "_GES", "_GEP", "_OSD"], + "nurbsCurve": ["_CRV"], + "nurbsSurface": ["_NRB"], + "locator": ["_LOC"], + "group": ["_GRP"] +} + +DEFAULT_PUBLISH_SETTINGS = { + "CollectMayaRender": { + "sync_workfile_version": False + }, + "CollectFbxCamera": { + "enabled": False + }, + "CollectGLTF": { + "enabled": False + }, + "ValidateInstanceInContext": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateContainers": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateFrameRange": { + "enabled": True, + "optional": True, + "active": True, + "exclude_product_types": [ + "model", + "rig", + "staticMesh" + ] + }, + "ValidateShaderName": { + "enabled": False, + "optional": True, + "active": True, + "regex": "(?P.*)_(.*)_SHD" + }, + "ValidateShadingEngine": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMayaColorSpace": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateAttributes": { + "enabled": False, + "attributes": "{}" + }, + "ValidateLoadedPlugin": { + "enabled": False, + "optional": True, + "whitelist_native_plugins": False, + "authorized_plugins": [] + }, + "ValidateMayaUnits": { + "enabled": True, + "optional": False, + "validate_linear_units": True, + "linear_units": "cm", + "validate_angular_units": True, + "angular_units": "deg", + "validate_fps": True + }, + "ValidateUnrealStaticMeshName": { + "enabled": True, + "optional": True, + "validate_mesh": False, + "validate_collision": True + }, + "ValidateCycleError": { + "enabled": True, + "optional": False, + "families": [ + "rig" + ] + }, + "ValidatePluginPathAttributes": { + "enabled": True, + "optional": False, + "active": True, + "attribute": [ + {"name": "AlembicNode", "value": "abc_File"}, + {"name": "VRayProxy", "value": "fileName"}, + {"name": "RenderManArchive", "value": "filename"}, + {"name": "pgYetiMaya", "value": "cacheFileName"}, + {"name": "aiStandIn", "value": "dso"}, + {"name": "RedshiftSprite", "value": "tex0"}, + {"name": "RedshiftBokeh", "value": "dofBokehImage"}, + {"name": "RedshiftCameraMap", "value": "tex0"}, + {"name": "RedshiftEnvironment", "value": "tex2"}, + {"name": "RedshiftDomeLight", "value": "tex1"}, + {"name": "RedshiftIESLight", "value": "profile"}, + {"name": "RedshiftLightGobo", "value": "tex0"}, + {"name": "RedshiftNormalMap", "value": "tex0"}, + {"name": "RedshiftProxyMesh", "value": "fileName"}, + {"name": "RedshiftVolumeShape", "value": "fileName"}, + {"name": "VRayTexGLSL", "value": "fileName"}, + {"name": "VRayMtlGLSL", "value": "fileName"}, + {"name": "VRayVRmatMtl", "value": "fileName"}, + {"name": "VRayPtex", "value": "ptexFile"}, + {"name": "VRayLightIESShape", "value": "iesFile"}, + {"name": "VRayMesh", "value": "materialAssignmentsFile"}, + {"name": "VRayMtlOSL", "value": "fileName"}, + {"name": "VRayTexOSL", "value": "fileName"}, + {"name": "VRayTexOCIO", "value": "ocioConfigFile"}, + {"name": "VRaySettingsNode", "value": "pmap_autoSaveFile2"}, + {"name": "VRayScannedMtl", "value": "file"}, + {"name": "VRayScene", "value": "parameterOverrideFilePath"}, + {"name": "VRayMtlMDL", "value": "filename"}, + {"name": "VRaySimbiont", "value": "file"}, + {"name": "dlOpenVDBShape", "value": "filename"}, + {"name": "pgYetiMayaShape", "value": "liveABCFilename"}, + {"name": "gpuCache", "value": "cacheFileName"}, + ] + }, + "ValidateRenderSettings": { + "arnold_render_attributes": [], + "vray_render_attributes": [], + "redshift_render_attributes": [], + "renderman_render_attributes": [] + }, + "ValidateCurrentRenderLayerIsRenderable": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateGLSLMaterial": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateGLSLPlugin": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateRenderImageRule": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateRenderNoDefaultCameras": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateRenderSingleCamera": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateRenderLayerAOVs": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateStepSize": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateVRayDistributedRendering": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateVrayReferencedAOVs": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateVRayTranslatorEnabled": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateVrayProxy": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateVrayProxyMembers": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateYetiRenderScriptCallbacks": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateYetiRigCacheState": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateYetiRigInputShapesInInstance": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateYetiRigSettings": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateModelName": { + "enabled": False, + "database": True, + "material_file": { + "windows": "", + "darwin": "", + "linux": "" + }, + "regex": "(.*)_(\\d)*_(?P.*)_(GEO)", + "top_level_regex": ".*_GRP" + }, + "ValidateModelContent": { + "enabled": True, + "optional": False, + "validate_top_group": True + }, + "ValidateTransformNamingSuffix": { + "enabled": True, + "optional": True, + "SUFFIX_NAMING_TABLE": json.dumps(DEFAULT_SUFFIX_NAMING, indent=4), + "ALLOW_IF_NOT_IN_SUFFIX_TABLE": True + }, + "ValidateColorSets": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshHasOverlappingUVs": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateMeshArnoldAttributes": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateMeshShaderConnections": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshSingleUVSet": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateMeshHasUVs": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshLaminaFaces": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateMeshNgons": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateMeshNonManifold": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateMeshNoNegativeScale": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateMeshNonZeroEdgeLength": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshNormalsUnlocked": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateMeshUVSetMap1": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateMeshVerticesHaveEdges": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateNoAnimation": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateNoNamespace": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateNoNullTransforms": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateNoUnknownNodes": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateNodeNoGhosting": { + "enabled": False, + "optional": False, + "active": True + }, + "ValidateShapeDefaultNames": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateShapeRenderStats": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateShapeZero": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateTransformZero": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateUniqueNames": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateNoVRayMesh": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateUnrealMeshTriangulated": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateAlembicVisibleOnly": { + "enabled": True, + "optional": False, + "active": True + }, + "ExtractProxyAlembic": { + "enabled": True, + "families": [ + "proxyAbc" + ] + }, + "ExtractAlembic": { + "enabled": True, + "families": [ + "pointcache", + "model", + "vrayproxy.alembic" + ] + }, + "ExtractObj": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateRigContents": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateRigJointsHidden": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateRigControllers": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateAnimationContent": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateOutRelatedNodeIds": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateRigControllersArnoldAttributes": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateSkeletalMeshHierarchy": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateSkinclusterDeformerSet": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateRigOutSetNodeIds": { + "enabled": True, + "optional": False, + "allow_history_only": False + }, + "ValidateCameraAttributes": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateAssemblyName": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateAssemblyNamespaces": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateAssemblyModelTransforms": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateAssRelativePaths": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateInstancerContent": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateInstancerFrameRanges": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateNoDefaultCameras": { + "enabled": True, + "optional": False, + "active": True + }, + "ValidateUnrealUpAxis": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateCameraContents": { + "enabled": True, + "optional": False, + "validate_shapes": True + }, + "ExtractPlayblast": DEFAULT_PLAYBLAST_SETTING, + "ExtractMayaSceneRaw": { + "enabled": True, + "add_for_families": [ + "layout" + ] + }, + "ExtractCameraAlembic": { + "enabled": True, + "optional": True, + "active": True, + "bake_attributes": "[]" + }, + "ExtractGLB": { + "enabled": True, + "active": True, + "ogsfx_path": "/maya2glTF/PBR/shaders/glTF_PBR.ogsfx" + }, + "ExtractLook": { + "maketx_arguments": [] + }, + "ExtractGPUCache": { + "enabled": False, + "families": [ + "model", + "animation", + "pointcache" + ], + "step": 1.0, + "stepSave": 1, + "optimize": True, + "optimizationThreshold": 40000, + "optimizeAnimationsForMotionBlur": True, + "writeMaterials": True, + "useBaseTessellation": True + } +} diff --git a/server_addon/maya/server/settings/render_settings.py b/server_addon/maya/server/settings/render_settings.py new file mode 100644 index 0000000000..b6163a04ce --- /dev/null +++ b/server_addon/maya/server/settings/render_settings.py @@ -0,0 +1,500 @@ +"""Providing models and values for Maya Render Settings.""" +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 arnold_image_format_enum(): + """Return enumerator for Arnold output formats.""" + return [ + {"label": "jpeg", "value": "jpeg"}, + {"label": "png", "value": "png"}, + {"label": "deepexr", "value": "deep exr"}, + {"label": "tif", "value": "tif"}, + {"label": "exr", "value": "exr"}, + {"label": "maya", "value": "maya"}, + {"label": "mtoa_shaders", "value": "mtoa_shaders"} + ] + + +def arnold_aov_list_enum(): + """Return enumerator for Arnold AOVs. + + Note: Key is value, Value in this case is Label. This + was taken from v3 settings. + """ + return [ + {"value": "empty", "label": "< empty >"}, + {"value": "ID", "label": "ID"}, + {"value": "N", "label": "N"}, + {"value": "P", "label": "P"}, + {"value": "Pref", "label": "Pref"}, + {"value": "RGBA", "label": "RGBA"}, + {"value": "Z", "label": "Z"}, + {"value": "albedo", "label": "albedo"}, + {"value": "background", "label": "background"}, + {"value": "coat", "label": "coat"}, + {"value": "coat_albedo", "label": "coat_albedo"}, + {"value": "coat_direct", "label": "coat_direct"}, + {"value": "coat_indirect", "label": "coat_indirect"}, + {"value": "cputime", "label": "cputime"}, + {"value": "crypto_asset", "label": "crypto_asset"}, + {"value": "crypto_material", "label": "cypto_material"}, + {"value": "crypto_object", "label": "crypto_object"}, + {"value": "diffuse", "label": "diffuse"}, + {"value": "diffuse_albedo", "label": "diffuse_albedo"}, + {"value": "diffuse_direct", "label": "diffuse_direct"}, + {"value": "diffuse_indirect", "label": "diffuse_indirect"}, + {"value": "direct", "label": "direct"}, + {"value": "emission", "label": "emission"}, + {"value": "highlight", "label": "highlight"}, + {"value": "indirect", "label": "indirect"}, + {"value": "motionvector", "label": "motionvector"}, + {"value": "opacity", "label": "opacity"}, + {"value": "raycount", "label": "raycount"}, + {"value": "rim_light", "label": "rim_light"}, + {"value": "shadow", "label": "shadow"}, + {"value": "shadow_diff", "label": "shadow_diff"}, + {"value": "shadow_mask", "label": "shadow_mask"}, + {"value": "shadow_matte", "label": "shadow_matte"}, + {"value": "sheen", "label": "sheen"}, + {"value": "sheen_albedo", "label": "sheen_albedo"}, + {"value": "sheen_direct", "label": "sheen_direct"}, + {"value": "sheen_indirect", "label": "sheen_indirect"}, + {"value": "specular", "label": "specular"}, + {"value": "specular_albedo", "label": "specular_albedo"}, + {"value": "specular_direct", "label": "specular_direct"}, + {"value": "specular_indirect", "label": "specular_indirect"}, + {"value": "sss", "label": "sss"}, + {"value": "sss_albedo", "label": "sss_albedo"}, + {"value": "sss_direct", "label": "sss_direct"}, + {"value": "sss_indirect", "label": "sss_indirect"}, + {"value": "transmission", "label": "transmission"}, + {"value": "transmission_albedo", "label": "transmission_albedo"}, + {"value": "transmission_direct", "label": "transmission_direct"}, + {"value": "transmission_indirect", "label": "transmission_indirect"}, + {"value": "volume", "label": "volume"}, + {"value": "volume_Z", "label": "volume_Z"}, + {"value": "volume_albedo", "label": "volume_albedo"}, + {"value": "volume_direct", "label": "volume_direct"}, + {"value": "volume_indirect", "label": "volume_indirect"}, + {"value": "volume_opacity", "label": "volume_opacity"}, + ] + + +def vray_image_output_enum(): + """Return output format for Vray enumerator.""" + return [ + {"label": "png", "value": "png"}, + {"label": "jpg", "value": "jpg"}, + {"label": "vrimg", "value": "vrimg"}, + {"label": "hdr", "value": "hdr"}, + {"label": "exr", "value": "exr"}, + {"label": "exr (multichannel)", "value": "exr (multichannel)"}, + {"label": "exr (deep)", "value": "exr (deep)"}, + {"label": "tga", "value": "tga"}, + {"label": "bmp", "value": "bmp"}, + {"label": "sgi", "value": "sgi"} + ] + + +def vray_aov_list_enum(): + """Return enumerator for Vray AOVs. + + Note: Key is value, Value in this case is Label. This + was taken from v3 settings. + """ + + return [ + {"value": "empty", "label": "< empty >"}, + {"value": "atmosphereChannel", "label": "atmosphere"}, + {"value": "backgroundChannel", "label": "background"}, + {"value": "bumpNormalsChannel", "label": "bumpnormals"}, + {"value": "causticsChannel", "label": "caustics"}, + {"value": "coatFilterChannel", "label": "coat_filter"}, + {"value": "coatGlossinessChannel", "label": "coatGloss"}, + {"value": "coatReflectionChannel", "label": "coat_reflection"}, + {"value": "vrayCoatChannel", "label": "coat_specular"}, + {"value": "CoverageChannel", "label": "coverage"}, + {"value": "cryptomatteChannel", "label": "cryptomatte"}, + {"value": "customColor", "label": "custom_color"}, + {"value": "drBucketChannel", "label": "DR"}, + {"value": "denoiserChannel", "label": "denoiser"}, + {"value": "diffuseChannel", "label": "diffuse"}, + {"value": "ExtraTexElement", "label": "extraTex"}, + {"value": "giChannel", "label": "GI"}, + {"value": "LightMixElement", "label": "None"}, + {"value": "lightingChannel", "label": "lighting"}, + {"value": "LightingAnalysisChannel", "label": "LightingAnalysis"}, + {"value": "materialIDChannel", "label": "materialID"}, + {"value": "MaterialSelectElement", "label": "materialSelect"}, + {"value": "matteShadowChannel", "label": "matteShadow"}, + {"value": "MultiMatteElement", "label": "multimatte"}, + {"value": "multimatteIDChannel", "label": "multimatteID"}, + {"value": "normalsChannel", "label": "normals"}, + {"value": "nodeIDChannel", "label": "objectId"}, + {"value": "objectSelectChannel", "label": "objectSelect"}, + {"value": "rawCoatFilterChannel", "label": "raw_coat_filter"}, + {"value": "rawCoatReflectionChannel", "label": "raw_coat_reflection"}, + {"value": "rawDiffuseFilterChannel", "label": "rawDiffuseFilter"}, + {"value": "rawGiChannel", "label": "rawGI"}, + {"value": "rawLightChannel", "label": "rawLight"}, + {"value": "rawReflectionChannel", "label": "rawReflection"}, + { + "value": "rawReflectionFilterChannel", + "label": "rawReflectionFilter" + }, + {"value": "rawRefractionChannel", "label": "rawRefraction"}, + { + "value": "rawRefractionFilterChannel", + "label": "rawRefractionFilter" + }, + {"value": "rawShadowChannel", "label": "rawShadow"}, + {"value": "rawSheenFilterChannel", "label": "raw_sheen_filter"}, + { + "value": "rawSheenReflectionChannel", + "label": "raw_sheen_reflection" + }, + {"value": "rawTotalLightChannel", "label": "rawTotalLight"}, + {"value": "reflectIORChannel", "label": "reflIOR"}, + {"value": "reflectChannel", "label": "reflect"}, + {"value": "reflectionFilterChannel", "label": "reflectionFilter"}, + {"value": "reflectGlossinessChannel", "label": "reflGloss"}, + {"value": "refractChannel", "label": "refract"}, + {"value": "refractionFilterChannel", "label": "refractionFilter"}, + {"value": "refractGlossinessChannel", "label": "refrGloss"}, + {"value": "renderIDChannel", "label": "renderId"}, + {"value": "FastSSS2Channel", "label": "SSS"}, + {"value": "sampleRateChannel", "label": "sampleRate"}, + {"value": "samplerInfo", "label": "samplerInfo"}, + {"value": "selfIllumChannel", "label": "selfIllum"}, + {"value": "shadowChannel", "label": "shadow"}, + {"value": "sheenFilterChannel", "label": "sheen_filter"}, + {"value": "sheenGlossinessChannel", "label": "sheenGloss"}, + {"value": "sheenReflectionChannel", "label": "sheen_reflection"}, + {"value": "vraySheenChannel", "label": "sheen_specular"}, + {"value": "specularChannel", "label": "specular"}, + {"value": "Toon", "label": "Toon"}, + {"value": "toonLightingChannel", "label": "toonLighting"}, + {"value": "toonSpecularChannel", "label": "toonSpecular"}, + {"value": "totalLightChannel", "label": "totalLight"}, + {"value": "unclampedColorChannel", "label": "unclampedColor"}, + {"value": "VRScansPaintMaskChannel", "label": "VRScansPaintMask"}, + {"value": "VRScansZoneMaskChannel", "label": "VRScansZoneMask"}, + {"value": "velocityChannel", "label": "velocity"}, + {"value": "zdepthChannel", "label": "zDepth"}, + {"value": "LightSelectElement", "label": "lightselect"}, + ] + + +def redshift_engine_enum(): + """Get Redshift engine type enumerator.""" + return [ + {"value": "0", "label": "None"}, + {"value": "1", "label": "Photon Map"}, + {"value": "2", "label": "Irradiance Cache"}, + {"value": "3", "label": "Brute Force"} + ] + + +def redshift_image_output_enum(): + """Return output format for Redshift enumerator.""" + return [ + {"value": "iff", "label": "Maya IFF"}, + {"value": "exr", "label": "OpenEXR"}, + {"value": "tif", "label": "TIFF"}, + {"value": "png", "label": "PNG"}, + {"value": "tga", "label": "Targa"}, + {"value": "jpg", "label": "JPEG"} + ] + + +def redshift_aov_list_enum(): + """Return enumerator for Vray AOVs. + + Note: Key is value, Value in this case is Label. This + was taken from v3 settings. + """ + return [ + {"value": "empty", "label": "< none >"}, + {"value": "AO", "label": "Ambient Occlusion"}, + {"value": "Background", "label": "Background"}, + {"value": "Beauty", "label": "Beauty"}, + {"value": "BumpNormals", "label": "Bump Normals"}, + {"value": "Caustics", "label": "Caustics"}, + {"value": "CausticsRaw", "label": "Caustics Raw"}, + {"value": "Cryptomatte", "label": "Cryptomatte"}, + {"value": "Custom", "label": "Custom"}, + {"value": "Z", "label": "Depth"}, + {"value": "DiffuseFilter", "label": "Diffuse Filter"}, + {"value": "DiffuseLighting", "label": "Diffuse Lighting"}, + {"value": "DiffuseLightingRaw", "label": "Diffuse Lighting Raw"}, + {"value": "Emission", "label": "Emission"}, + {"value": "GI", "label": "Global Illumination"}, + {"value": "GIRaw", "label": "Global Illumination Raw"}, + {"value": "Matte", "label": "Matte"}, + {"value": "MotionVectors", "label": "Ambient Occlusion"}, + {"value": "N", "label": "Normals"}, + {"value": "ID", "label": "ObjectID"}, + {"value": "ObjectBumpNormal", "label": "Object-Space Bump Normals"}, + {"value": "ObjectPosition", "label": "Object-Space Positions"}, + {"value": "PuzzleMatte", "label": "Puzzle Matte"}, + {"value": "Reflections", "label": "Reflections"}, + {"value": "ReflectionsFilter", "label": "Reflections Filter"}, + {"value": "ReflectionsRaw", "label": "Reflections Raw"}, + {"value": "Refractions", "label": "Refractions"}, + {"value": "RefractionsFilter", "label": "Refractions Filter"}, + {"value": "RefractionsRaw", "label": "Refractions Filter"}, + {"value": "Shadows", "label": "Shadows"}, + {"value": "SpecularLighting", "label": "Specular Lighting"}, + {"value": "SSS", "label": "Sub Surface Scatter"}, + {"value": "SSSRaw", "label": "Sub Surface Scatter Raw"}, + { + "value": "TotalDiffuseLightingRaw", + "label": "Total Diffuse Lighting Raw" + }, + { + "value": "TotalTransLightingRaw", + "label": "Total Translucency Filter" + }, + {"value": "TransTint", "label": "Translucency Filter"}, + {"value": "TransGIRaw", "label": "Translucency Lighting Raw"}, + {"value": "VolumeFogEmission", "label": "Volume Fog Emission"}, + {"value": "VolumeFogTint", "label": "Volume Fog Tint"}, + {"value": "VolumeLighting", "label": "Volume Lighting"}, + {"value": "P", "label": "World Position"}, + ] + + +class AdditionalOptionsModel(BaseSettingsModel): + """Additional Option""" + _layout = "compact" + + attribute: str = Field("", title="Attribute name") + value: str = Field("", title="Value") + + +class ArnoldSettingsModel(BaseSettingsModel): + image_prefix: str = Field(title="Image prefix template") + image_format: str = Field( + enum_resolver=arnold_image_format_enum, title="Output Image Format") + multilayer_exr: bool = Field(title="Multilayer (exr)") + tiled: bool = Field(title="Tiled (tif, exr)") + aov_list: list[str] = Field( + default_factory=list, + enum_resolver=arnold_aov_list_enum, + title="AOVs to create" + ) + additional_options: list[AdditionalOptionsModel] = Field( + default_factory=list, + title="Additional Arnold Options", + description=( + "Add additional options - put attribute and value, like AASamples" + ) + ) + + +class VraySettingsModel(BaseSettingsModel): + image_prefix: str = Field(title="Image prefix template") + # engine was str because of JSON limitation (key must be string) + engine: str = Field( + enum_resolver=lambda: [ + {"label": "V-Ray", "value": "1"}, + {"label": "V-Ray GPU", "value": "2"} + ], + title="Production Engine" + ) + image_format: str = Field( + enum_resolver=vray_image_output_enum, + title="Output Image Format" + ) + aov_list: list[str] = Field( + default_factory=list, + enum_resolver=vray_aov_list_enum, + title="AOVs to create" + ) + additional_options: list[AdditionalOptionsModel] = Field( + default_factory=list, + title="Additional Vray Options", + description=( + "Add additional options - put attribute and value," + " like aaFilterSize" + ) + ) + + +class RedshiftSettingsModel(BaseSettingsModel): + image_prefix: str = Field(title="Image prefix template") + # both engines are using the same enumerator, + # both were originally str because of JSON limitation. + primary_gi_engine: str = Field( + enum_resolver=redshift_engine_enum, + title="Primary GI Engine" + ) + secondary_gi_engine: str = Field( + enum_resolver=redshift_engine_enum, + title="Secondary GI Engine" + ) + image_format: str = Field( + enum_resolver=redshift_image_output_enum, + title="Output Image Format" + ) + multilayer_exr: bool = Field(title="Multilayer (exr)") + force_combine: bool = Field(title="Force combine beauty and AOVs") + aov_list: list[str] = Field( + default_factory=list, + enum_resolver=redshift_aov_list_enum, + title="AOVs to create" + ) + additional_options: list[AdditionalOptionsModel] = Field( + default_factory=list, + title="Additional Vray Options", + description=( + "Add additional options - put attribute and value," + " like reflectionMaxTraceDepth" + ) + ) + + +def renderman_display_filters(): + return [ + "PxrBackgroundDisplayFilter", + "PxrCopyAOVDisplayFilter", + "PxrEdgeDetect", + "PxrFilmicTonemapperDisplayFilter", + "PxrGradeDisplayFilter", + "PxrHalfBufferErrorFilter", + "PxrImageDisplayFilter", + "PxrLightSaturation", + "PxrShadowDisplayFilter", + "PxrStylizedHatching", + "PxrStylizedLines", + "PxrStylizedToon", + "PxrWhitePointDisplayFilter" + ] + + +def renderman_sample_filters_enum(): + return [ + "PxrBackgroundSampleFilter", + "PxrCopyAOVSampleFilter", + "PxrCryptomatte", + "PxrFilmicTonemapperSampleFilter", + "PxrGradeSampleFilter", + "PxrShadowFilter", + "PxrWatermarkFilter", + "PxrWhitePointSampleFilter" + ] + + +class RendermanSettingsModel(BaseSettingsModel): + image_prefix: str = Field( + "", title="Image prefix template") + image_dir: str = Field( + "", title="Image Output Directory") + display_filters: list[str] = Field( + default_factory=list, + title="Display Filters", + enum_resolver=renderman_display_filters + ) + imageDisplay_dir: str = Field( + "", title="Image Display Filter Directory") + sample_filters: list[str] = Field( + default_factory=list, + title="Sample Filters", + enum_resolver=renderman_sample_filters_enum + ) + cryptomatte_dir: str = Field( + "", title="Cryptomatte Output Directory") + watermark_dir: str = Field( + "", title="Watermark Filter Directory") + additional_options: list[AdditionalOptionsModel] = Field( + default_factory=list, + title="Additional Renderer Options" + ) + + +class RenderSettingsModel(BaseSettingsModel): + apply_render_settings: bool = Field( + title="Apply Render Settings on creation" + ) + default_render_image_folder: str = Field( + title="Default render image folder" + ) + enable_all_lights: bool = Field( + title="Include all lights in Render Setup Layers by default" + ) + aov_separator: str = Field( + "underscore", + title="AOV Separator character", + enum_resolver=aov_separators_enum + ) + reset_current_frame: bool = Field( + title="Reset Current Frame") + remove_aovs: bool = Field( + title="Remove existing AOVs") + arnold_renderer: ArnoldSettingsModel = Field( + default_factory=ArnoldSettingsModel, + title="Arnold Renderer") + vray_renderer: VraySettingsModel = Field( + default_factory=VraySettingsModel, + title="Vray Renderer") + redshift_renderer: RedshiftSettingsModel = Field( + default_factory=RedshiftSettingsModel, + title="Redshift Renderer") + renderman_renderer: RendermanSettingsModel = Field( + default_factory=RendermanSettingsModel, + title="Renderman Renderer") + + +DEFAULT_RENDER_SETTINGS = { + "apply_render_settings": True, + "default_render_image_folder": "renders/maya", + "enable_all_lights": True, + "aov_separator": "underscore", + "reset_current_frame": False, + "remove_aovs": False, + "arnold_renderer": { + "image_prefix": "//_", + "image_format": "exr", + "multilayer_exr": True, + "tiled": True, + "aov_list": [], + "additional_options": [] + }, + "vray_renderer": { + "image_prefix": "//", + "engine": "1", + "image_format": "exr", + "aov_list": [], + "additional_options": [] + }, + "redshift_renderer": { + "image_prefix": "//", + "primary_gi_engine": "0", + "secondary_gi_engine": "0", + "image_format": "exr", + "multilayer_exr": True, + "force_combine": True, + "aov_list": [], + "additional_options": [] + }, + "renderman_renderer": { + "image_prefix": "{aov_separator}..", + "image_dir": "/", + "display_filters": [], + "imageDisplay_dir": "/{aov_separator}imageDisplayFilter..", + "sample_filters": [], + "cryptomatte_dir": "/{aov_separator}cryptomatte..", + "watermark_dir": "/{aov_separator}watermarkFilter..", + "additional_options": [] + } +} diff --git a/server_addon/maya/server/settings/scriptsmenu.py b/server_addon/maya/server/settings/scriptsmenu.py new file mode 100644 index 0000000000..82c1c2e53c --- /dev/null +++ b/server_addon/maya/server/settings/scriptsmenu.py @@ -0,0 +1,43 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class ScriptsmenuSubmodel(BaseSettingsModel): + """Item Definition""" + _isGroup = True + type: str = Field(title="Type") + command: str = Field(title="Command") + sourcetype: str = Field(title="Source Type") + title: str = Field(title="Title") + tooltip: str = Field(title="Tooltip") + tags: list[str] = Field(default_factory=list, title="A list of tags") + + +class ScriptsmenuModel(BaseSettingsModel): + _isGroup = True + + name: str = Field(title="Menu Name") + definition: list[ScriptsmenuSubmodel] = Field( + default_factory=list, + title="Menu Definition", + description="Scriptmenu Items Definition" + ) + + +DEFAULT_SCRIPTSMENU_SETTINGS = { + "name": "OpenPype Tools", + "definition": [ + { + "type": "action", + "command": "import openpype.hosts.maya.api.commands as op_cmds; op_cmds.edit_shader_definitions()", + "sourcetype": "python", + "title": "Edit shader name definitions", + "tooltip": "Edit shader name definitions used in validation and renaming.", + "tags": [ + "pipeline", + "shader" + ] + } + ] +} diff --git a/server_addon/maya/server/settings/templated_workfile_settings.py b/server_addon/maya/server/settings/templated_workfile_settings.py new file mode 100644 index 0000000000..ef81b31a07 --- /dev/null +++ b/server_addon/maya/server/settings/templated_workfile_settings.py @@ -0,0 +1,25 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel, task_types_enum + + +class WorkfileBuildProfilesModel(BaseSettingsModel): + _layout = "expanded" + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field(default_factory=list, title="Task names") + path: str = Field("", title="Path to template") + + +class TemplatedProfilesModel(BaseSettingsModel): + profiles: list[WorkfileBuildProfilesModel] = Field( + default_factory=list, + title="Profiles" + ) + + +DEFAULT_TEMPLATED_WORKFILE_SETTINGS = { + "profiles": [] +} diff --git a/server_addon/maya/server/settings/workfile_build_settings.py b/server_addon/maya/server/settings/workfile_build_settings.py new file mode 100644 index 0000000000..dc56d1a320 --- /dev/null +++ b/server_addon/maya/server/settings/workfile_build_settings.py @@ -0,0 +1,131 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel, task_types_enum + + +class ContextItemModel(BaseSettingsModel): + _layout = "expanded" + product_name_filters: list[str] = Field( + default_factory=list, title="Product name Filters") + product_types: list[str] = Field( + default_factory=list, title="Product types") + repre_names: list[str] = Field( + default_factory=list, title="Repre Names") + loaders: list[str] = Field( + default_factory=list, title="Loaders") + + +class WorkfileSettingModel(BaseSettingsModel): + _layout = "expanded" + task_types: list[str] = Field( + default_factory=list, + enum_resolver=task_types_enum, + title="Task types") + tasks: list[str] = Field( + default_factory=list, + title="Task names") + current_context: list[ContextItemModel] = Field( + default_factory=list, + title="Current Context") + linked_assets: list[ContextItemModel] = Field( + default_factory=list, + title="Linked Assets") + + +class ProfilesModel(BaseSettingsModel): + profiles: list[WorkfileSettingModel] = Field( + default_factory=list, + title="Profiles" + ) + + +DEFAULT_WORKFILE_SETTING = { + "profiles": [ + { + "task_types": [], + "tasks": [ + "Lighting" + ], + "current_context": [ + { + "product_name_filters": [ + ".+[Mm]ain" + ], + "product_types": [ + "model" + ], + "repre_names": [ + "abc", + "ma" + ], + "loaders": [ + "ReferenceLoader" + ] + }, + { + "product_name_filters": [], + "product_types": [ + "animation", + "pointcache", + "proxyAbc" + ], + "repre_names": [ + "abc" + ], + "loaders": [ + "ReferenceLoader" + ] + }, + { + "product_name_filters": [], + "product_types": [ + "rendersetup" + ], + "repre_names": [ + "json" + ], + "loaders": [ + "RenderSetupLoader" + ] + }, + { + "product_name_filters": [], + "product_types": [ + "camera" + ], + "repre_names": [ + "abc" + ], + "loaders": [ + "ReferenceLoader" + ] + } + ], + "linked_assets": [ + { + "product_name_filters": [], + "product_types": [ + "sedress" + ], + "repre_names": [ + "ma" + ], + "loaders": [ + "ReferenceLoader" + ] + }, + { + "product_name_filters": [], + "product_types": [ + "ArnoldStandin" + ], + "repre_names": [ + "ass" + ], + "loaders": [ + "assLoader" + ] + } + ] + } + ] +} diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py new file mode 100644 index 0000000000..d4b9e2d7f3 --- /dev/null +++ b/server_addon/maya/server/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Package declaring addon version.""" +__version__ = "0.1.0" diff --git a/server_addon/muster/server/__init__.py b/server_addon/muster/server/__init__.py new file mode 100644 index 0000000000..2cb8943554 --- /dev/null +++ b/server_addon/muster/server/__init__.py @@ -0,0 +1,17 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import MusterSettings, DEFAULT_VALUES + + +class MusterAddon(BaseServerAddon): + name = "muster" + version = __version__ + title = "Muster" + settings_model: Type[MusterSettings] = MusterSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/muster/server/settings.py b/server_addon/muster/server/settings.py new file mode 100644 index 0000000000..f3f6660abc --- /dev/null +++ b/server_addon/muster/server/settings.py @@ -0,0 +1,37 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class TemplatesMapping(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: int = Field(title="mapping") + + +class MusterSettings(BaseSettingsModel): + enabled: bool = True + MUSTER_REST_URL: str = Field("", title="Muster Rest URL") + + templates_mapping: list[TemplatesMapping] = Field( + default_factory=list, + title="Templates mapping", + ) + + +DEFAULT_VALUES = { + "enabled": False, + "MUSTER_REST_URL": "http://127.0.0.1:9890", + "templates_mapping": [ + {"name": "file_layers", "value": 7}, + {"name": "mentalray", "value": 2}, + {"name": "mentalray_sf", "value": 6}, + {"name": "redshift", "value": 55}, + {"name": "renderman", "value": 29}, + {"name": "software", "value": 1}, + {"name": "software_sf", "value": 5}, + {"name": "turtle", "value": 10}, + {"name": "vector", "value": 4}, + {"name": "vray", "value": 37}, + {"name": "ffmpeg", "value": 48} + ] +} diff --git a/server_addon/muster/server/version.py b/server_addon/muster/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/muster/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/nuke/server/__init__.py b/server_addon/nuke/server/__init__.py new file mode 100644 index 0000000000..032ceea5fb --- /dev/null +++ b/server_addon/nuke/server/__init__.py @@ -0,0 +1,17 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import NukeSettings, DEFAULT_VALUES + + +class NukeAddon(BaseServerAddon): + name = "nuke" + title = "Nuke" + version = __version__ + settings_model: Type[NukeSettings] = NukeSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/nuke/server/settings/__init__.py b/server_addon/nuke/server/settings/__init__.py new file mode 100644 index 0000000000..1e58865395 --- /dev/null +++ b/server_addon/nuke/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + NukeSettings, + DEFAULT_VALUES, +) + + +__all__ = ( + "NukeSettings", + "DEFAULT_VALUES", +) diff --git a/server_addon/nuke/server/settings/common.py b/server_addon/nuke/server/settings/common.py new file mode 100644 index 0000000000..f1bb46ff90 --- /dev/null +++ b/server_addon/nuke/server/settings/common.py @@ -0,0 +1,128 @@ +import json +from pydantic import Field +from ayon_server.exceptions import BadRequestException +from ayon_server.settings import BaseSettingsModel +from ayon_server.types import ( + ColorRGBA_float, + ColorRGB_uint8 +) + + +def validate_json_dict(value): + if not value.strip(): + return "{}" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, dict) + except json.JSONDecodeError: + success = False + + if not success: + raise BadRequestException( + "Environment's can't be parsed as json object" + ) + return value + + +class Vector2d(BaseSettingsModel): + _layout = "compact" + + x: float = Field(1.0, title="X") + y: float = Field(1.0, title="Y") + + +class Vector3d(BaseSettingsModel): + _layout = "compact" + + x: float = Field(1.0, title="X") + y: float = Field(1.0, title="Y") + z: float = Field(1.0, title="Z") + + +def formatable_knob_type_enum(): + return [ + {"value": "text", "label": "Text"}, + {"value": "number", "label": "Number"}, + {"value": "decimal_number", "label": "Decimal number"}, + {"value": "2d_vector", "label": "2D vector"}, + # "3D vector" + ] + + +class Formatable(BaseSettingsModel): + _layout = "compact" + + template: str = Field( + "", + placeholder="""{{key}} or {{key}};{{key}}""", + title="Template" + ) + to_type: str = Field( + "Text", + title="To Knob type", + enum_resolver=formatable_knob_type_enum, + ) + + +knob_types_enum = [ + {"value": "text", "label": "Text"}, + {"value": "formatable", "label": "Formate from template"}, + {"value": "color_gui", "label": "Color GUI"}, + {"value": "boolean", "label": "Boolean"}, + {"value": "number", "label": "Number"}, + {"value": "decimal_number", "label": "Decimal number"}, + {"value": "vector_2d", "label": "2D vector"}, + {"value": "vector_3d", "label": "3D vector"}, + {"value": "color", "label": "Color"}, + {"value": "expression", "label": "Expression"} +] + + +class KnobModel(BaseSettingsModel): + """# TODO: new data structure + - v3 was having type, name, value but + ayon is not able to make it the same. Current model is + defining `type` as `text` and instead of `value` the key is `text`. + So if `type` is `boolean` then key is `boolean` (value). + """ + _layout = "expanded" + + type: str = Field( + title="Type", + description="Switch between different knob types", + enum_resolver=lambda: knob_types_enum, + conditionalEnum=True + ) + + name: str = Field( + title="Name", + placeholder="Name" + ) + text: str = Field("", title="Value") + color_gui: ColorRGB_uint8 = Field( + (0, 0, 255), + title="RGB Uint8", + ) + boolean: bool = Field(False, title="Value") + number: int = Field(0, title="Value") + decimal_number: float = Field(0.0, title="Value") + vector_2d: Vector2d = Field( + default_factory=Vector2d, + title="Value" + ) + vector_3d: Vector3d = Field( + default_factory=Vector3d, + title="Value" + ) + color: ColorRGBA_float = Field( + (0.0, 0.0, 1.0, 1.0), + title="RGBA Float" + ) + formatable: Formatable = Field( + default_factory=Formatable, + title="Formatable" + ) + expression: str = Field( + "", + title="Expression" + ) diff --git a/server_addon/nuke/server/settings/create_plugins.py b/server_addon/nuke/server/settings/create_plugins.py new file mode 100644 index 0000000000..0bbae4ee77 --- /dev/null +++ b/server_addon/nuke/server/settings/create_plugins.py @@ -0,0 +1,223 @@ +from pydantic import validator, Field +from ayon_server.settings import ( + BaseSettingsModel, + ensure_unique_names +) +from .common import KnobModel + + +def instance_attributes_enum(): + """Return create write instance attributes.""" + return [ + {"value": "reviewable", "label": "Reviewable"}, + {"value": "farm_rendering", "label": "Farm rendering"}, + {"value": "use_range_limit", "label": "Use range limit"} + ] + + +class PrenodeModel(BaseSettingsModel): + # TODO: missing in host api + # - good for `dependency` + name: str = Field( + title="Node name" + ) + + # TODO: `nodeclass` should be renamed to `nuke_node_class` + nodeclass: str = Field( + "", + title="Node class" + ) + dependent: str = Field( + "", + title="Incoming dependency" + ) + + """# TODO: Changes in host api: + - Need complete rework of knob types in nuke integration. + - We could not support v3 style of settings. + """ + knobs: list[KnobModel] = Field( + title="Knobs", + ) + + @validator("knobs") + def ensure_unique_names(cls, value): + """Ensure name fields within the lists have unique names.""" + ensure_unique_names(value) + return value + + +class CreateWriteRenderModel(BaseSettingsModel): + temp_rendering_path_template: str = Field( + title="Temporary rendering path template" + ) + default_variants: list[str] = Field( + title="Default variants", + default_factory=list + ) + instance_attributes: list[str] = Field( + default_factory=list, + enum_resolver=instance_attributes_enum, + title="Instance attributes" + ) + + """# TODO: Changes in host api: + - prenodes key was originally dict and now is list + (we could not support v3 style of settings) + """ + prenodes: list[PrenodeModel] = Field( + title="Preceding nodes", + ) + + @validator("prenodes") + def ensure_unique_names(cls, value): + """Ensure name fields within the lists have unique names.""" + ensure_unique_names(value) + return value + + +class CreateWritePrerenderModel(BaseSettingsModel): + temp_rendering_path_template: str = Field( + title="Temporary rendering path template" + ) + default_variants: list[str] = Field( + title="Default variants", + default_factory=list + ) + instance_attributes: list[str] = Field( + default_factory=list, + enum_resolver=instance_attributes_enum, + title="Instance attributes" + ) + + """# TODO: Changes in host api: + - prenodes key was originally dict and now is list + (we could not support v3 style of settings) + """ + prenodes: list[PrenodeModel] = Field( + title="Preceding nodes", + ) + + @validator("prenodes") + def ensure_unique_names(cls, value): + """Ensure name fields within the lists have unique names.""" + ensure_unique_names(value) + return value + + +class CreateWriteImageModel(BaseSettingsModel): + temp_rendering_path_template: str = Field( + title="Temporary rendering path template" + ) + default_variants: list[str] = Field( + title="Default variants", + default_factory=list + ) + instance_attributes: list[str] = Field( + default_factory=list, + enum_resolver=instance_attributes_enum, + title="Instance attributes" + ) + + """# TODO: Changes in host api: + - prenodes key was originally dict and now is list + (we could not support v3 style of settings) + """ + prenodes: list[PrenodeModel] = Field( + title="Preceding nodes", + ) + + @validator("prenodes") + def ensure_unique_names(cls, value): + """Ensure name fields within the lists have unique names.""" + ensure_unique_names(value) + return value + + +class CreatorPluginsSettings(BaseSettingsModel): + CreateWriteRender: CreateWriteRenderModel = Field( + default_factory=CreateWriteRenderModel, + title="Create Write Render" + ) + CreateWritePrerender: CreateWritePrerenderModel = Field( + default_factory=CreateWritePrerenderModel, + title="Create Write Prerender" + ) + CreateWriteImage: CreateWriteImageModel = Field( + default_factory=CreateWriteImageModel, + title="Create Write Image" + ) + + +DEFAULT_CREATE_SETTINGS = { + "CreateWriteRender": { + "temp_rendering_path_template": "{work}/renders/nuke/{product[name]}/{product[name]}.{frame}.{ext}", + "default_variants": [ + "Main", + "Mask" + ], + "instance_attributes": [ + "reviewable", + "farm_rendering" + ], + "prenodes": [ + { + "name": "Reformat01", + "nodeclass": "Reformat", + "dependent": "", + "knobs": [ + { + "type": "text", + "name": "resize", + "text": "none" + }, + { + "type": "boolean", + "name": "black_outside", + "boolean": True + } + ] + } + ] + }, + "CreateWritePrerender": { + "temp_rendering_path_template": "{work}/renders/nuke/{product[name]}/{product[name]}.{frame}.{ext}", + "default_variants": [ + "Key01", + "Bg01", + "Fg01", + "Branch01", + "Part01" + ], + "instance_attributes": [ + "farm_rendering", + "use_range_limit" + ], + "prenodes": [] + }, + "CreateWriteImage": { + "temp_rendering_path_template": "{work}/renders/nuke/{product[name]}/{product[name]}.{ext}", + "default_variants": [ + "StillFrame", + "MPFrame", + "LayoutFrame" + ], + "instance_attributes": [ + "use_range_limit" + ], + "prenodes": [ + { + "name": "FrameHold01", + "nodeclass": "FrameHold", + "dependent": "", + "knobs": [ + { + "type": "expression", + "name": "first_frame", + "expression": "parent.first" + } + ] + } + ] + } +} diff --git a/server_addon/nuke/server/settings/dirmap.py b/server_addon/nuke/server/settings/dirmap.py new file mode 100644 index 0000000000..2da6d7bf60 --- /dev/null +++ b/server_addon/nuke/server/settings/dirmap.py @@ -0,0 +1,47 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class DirmapPathsSubmodel(BaseSettingsModel): + _layout = "compact" + source_path: list[str] = Field( + default_factory=list, + title="Source Paths" + ) + destination_path: list[str] = Field( + default_factory=list, + title="Destination Paths" + ) + + +class DirmapSettings(BaseSettingsModel): + """Nuke color management project settings.""" + _isGroup: bool = True + + enabled: bool = Field(title="enabled") + paths: DirmapPathsSubmodel = Field( + default_factory=DirmapPathsSubmodel, + title="Dirmap Paths" + ) + + +"""# TODO: +nuke is having originally implemented +following data inputs: + +"nuke-dirmap": { + "enabled": false, + "paths": { + "source-path": [], + "destination-path": [] + } +} +""" + +DEFAULT_DIRMAP_SETTINGS = { + "enabled": False, + "paths": { + "source_path": [], + "destination_path": [] + } +} diff --git a/server_addon/nuke/server/settings/filters.py b/server_addon/nuke/server/settings/filters.py new file mode 100644 index 0000000000..7e2702b3b7 --- /dev/null +++ b/server_addon/nuke/server/settings/filters.py @@ -0,0 +1,19 @@ +from pydantic import Field, validator +from ayon_server.settings import BaseSettingsModel, ensure_unique_names + + +class PublishGUIFilterItemModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: bool = Field(True, title="Active") + + +class PublishGUIFiltersModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: list[PublishGUIFilterItemModel] = Field(default_factory=list) + + @validator("value") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value diff --git a/server_addon/nuke/server/settings/general.py b/server_addon/nuke/server/settings/general.py new file mode 100644 index 0000000000..bcbb183952 --- /dev/null +++ b/server_addon/nuke/server/settings/general.py @@ -0,0 +1,42 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class MenuShortcut(BaseSettingsModel): + """Nuke general project settings.""" + + create: str = Field( + title="Create..." + ) + publish: str = Field( + title="Publish..." + ) + load: str = Field( + title="Load..." + ) + manage: str = Field( + title="Manage..." + ) + build_workfile: str = Field( + title="Build Workfile..." + ) + + +class GeneralSettings(BaseSettingsModel): + """Nuke general project settings.""" + + menu: MenuShortcut = Field( + default_factory=MenuShortcut, + title="Menu Shortcuts", + ) + + +DEFAULT_GENERAL_SETTINGS = { + "menu": { + "create": "ctrl+alt+c", + "publish": "ctrl+alt+p", + "load": "ctrl+alt+l", + "manage": "ctrl+alt+m", + "build_workfile": "ctrl+alt+b" + } +} diff --git a/server_addon/nuke/server/settings/gizmo.py b/server_addon/nuke/server/settings/gizmo.py new file mode 100644 index 0000000000..4cdd614da8 --- /dev/null +++ b/server_addon/nuke/server/settings/gizmo.py @@ -0,0 +1,79 @@ +from pydantic import Field +from ayon_server.settings import ( + BaseSettingsModel, + MultiplatformPathModel, + MultiplatformPathListModel, +) + + +class SubGizmoItem(BaseSettingsModel): + title: str = Field( + title="Label" + ) + sourcetype: str = Field( + title="Type of usage" + ) + command: str = Field( + title="Python command" + ) + icon: str = Field( + title="Icon Path" + ) + shortcut: str = Field( + title="Hotkey" + ) + + +class GizmoDefinitionItem(BaseSettingsModel): + gizmo_toolbar_path: str = Field( + title="Gizmo Menu" + ) + sub_gizmo_list: list[SubGizmoItem] = Field( + default_factory=list, title="Sub Gizmo List") + + +class GizmoItem(BaseSettingsModel): + """Nuke gizmo item """ + + toolbar_menu_name: str = Field( + title="Toolbar Menu Name" + ) + gizmo_source_dir: MultiplatformPathListModel = Field( + default_factory=MultiplatformPathListModel, + title="Gizmo Directory Path" + ) + toolbar_icon_path: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Toolbar Icon Path" + ) + gizmo_definition: list[GizmoDefinitionItem] = Field( + default_factory=list, title="Gizmo Definition") + + +DEFAULT_GIZMO_ITEM = { + "toolbar_menu_name": "OpenPype Gizmo", + "gizmo_source_dir": { + "windows": [], + "darwin": [], + "linux": [] + }, + "toolbar_icon_path": { + "windows": "", + "darwin": "", + "linux": "" + }, + "gizmo_definition": [ + { + "gizmo_toolbar_path": "/path/to/menu", + "sub_gizmo_list": [ + { + "sourcetype": "python", + "title": "Gizmo Note", + "command": "nuke.nodes.StickyNote(label='You can create your own toolbar menu in the Nuke GizmoMenu of OpenPype')", + "icon": "", + "shortcut": "" + } + ] + } + ] +} diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py new file mode 100644 index 0000000000..b43017ef8b --- /dev/null +++ b/server_addon/nuke/server/settings/imageio.py @@ -0,0 +1,410 @@ +from typing import Literal +from pydantic import validator, Field +from ayon_server.settings import ( + BaseSettingsModel, + ensure_unique_names, +) + +from .common import KnobModel + + +class NodesModel(BaseSettingsModel): + """# TODO: This needs to be somehow labeled in settings panel + or at least it could show gist of configuration + """ + _layout = "expanded" + plugins: list[str] = Field( + title="Used in plugins" + ) + # TODO: rename `nukeNodeClass` to `nuke_node_class` + nukeNodeClass: str = Field( + title="Nuke Node Class", + ) + + """ # TODO: Need complete rework of knob types + in nuke integration. We could not support v3 style of settings. + """ + knobs: list[KnobModel] = Field( + title="Knobs", + ) + + @validator("knobs") + def ensure_unique_names(cls, value): + """Ensure name fields within the lists have unique names.""" + ensure_unique_names(value) + return value + + +class NodesSetting(BaseSettingsModel): + # TODO: rename `requiredNodes` to `required_nodes` + requiredNodes: list[NodesModel] = Field( + title="Plugin required", + default_factory=list + ) + # TODO: rename `overrideNodes` to `override_nodes` + overrideNodes: list[NodesModel] = Field( + title="Plugin's node overrides", + default_factory=list + ) + + +def ocio_configs_switcher_enum(): + return [ + {"value": "nuke-default", "label": "nuke-default"}, + {"value": "spi-vfx", "label": "spi-vfx"}, + {"value": "spi-anim", "label": "spi-anim"}, + {"value": "aces_0.1.1", "label": "aces_0.1.1"}, + {"value": "aces_0.7.1", "label": "aces_0.7.1"}, + {"value": "aces_1.0.1", "label": "aces_1.0.1"}, + {"value": "aces_1.0.3", "label": "aces_1.0.3"}, + {"value": "aces_1.1", "label": "aces_1.1"}, + {"value": "aces_1.2", "label": "aces_1.2"}, + {"value": "aces_1.3", "label": "aces_1.3"}, + {"value": "custom", "label": "custom"} + ] + + +class WorkfileColorspaceSettings(BaseSettingsModel): + """Nuke workfile colorspace preset. """ + """# TODO: enhance settings with host api: + we need to add mapping to resolve properly keys. + Nuke is excpecting camel case key names, + but for better code consistency we need to + be using snake_case: + + color_management = colorManagement + ocio_config = OCIO_config + working_space_name = workingSpaceLUT + monitor_name = monitorLut + monitor_out_name = monitorOutLut + int_8_name = int8Lut + int_16_name = int16Lut + log_name = logLut + float_name = floatLut + """ + + colorManagement: Literal["Nuke", "OCIO"] = Field( + title="Color Management" + ) + + OCIO_config: str = Field( + title="OpenColorIO Config", + description="Switch between OCIO configs", + enum_resolver=ocio_configs_switcher_enum, + conditionalEnum=True + ) + + workingSpaceLUT: str = Field( + title="Working Space" + ) + monitorLut: str = Field( + title="Monitor" + ) + int8Lut: str = Field( + title="8-bit files" + ) + int16Lut: str = Field( + title="16-bit files" + ) + logLut: str = Field( + title="Log files" + ) + floatLut: str = Field( + title="Float files" + ) + + +class ReadColorspaceRulesItems(BaseSettingsModel): + _layout = "expanded" + + regex: str = Field("", title="Regex expression") + colorspace: str = Field("", title="Colorspace") + + +class RegexInputsModel(BaseSettingsModel): + inputs: list[ReadColorspaceRulesItems] = Field( + default_factory=list, + title="Inputs" + ) + + +class ViewProcessModel(BaseSettingsModel): + viewerProcess: str = Field( + title="Viewer Process Name" + ) + + +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): + """Nuke color management project settings. """ + _isGroup: bool = True + + """# TODO: enhance settings with host api: + to restruture settings for simplification. + + now: nuke/imageio/viewer/viewerProcess + future: nuke/imageio/viewer + """ + 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" + ) + viewer: ViewProcessModel = Field( + default_factory=ViewProcessModel, + title="Viewer", + description="""Viewer profile is used during + Creation of new viewer node at knob viewerProcess""" + ) + + """# TODO: enhance settings with host api: + to restruture settings for simplification. + + now: nuke/imageio/baking/viewerProcess + future: nuke/imageio/baking + """ + baking: ViewProcessModel = Field( + default_factory=ViewProcessModel, + title="Baking", + description="""Baking profile is used during + publishing baked colorspace data at knob viewerProcess""" + ) + + workfile: WorkfileColorspaceSettings = Field( + default_factory=WorkfileColorspaceSettings, + title="Workfile" + ) + + nodes: NodesSetting = Field( + default_factory=NodesSetting, + title="Nodes" + ) + """# TODO: enhance settings with host api: + - old settings are using `regexInputs` key but we + need to rename to `regex_inputs` + - no need for `inputs` middle part. It can stay + directly on `regex_inputs` + """ + regexInputs: RegexInputsModel = Field( + default_factory=RegexInputsModel, + title="Assign colorspace to read nodes via rules" + ) + + +DEFAULT_IMAGEIO_SETTINGS = { + "viewer": { + "viewerProcess": "sRGB" + }, + "baking": { + "viewerProcess": "rec709" + }, + "workfile": { + "colorManagement": "Nuke", + "OCIO_config": "nuke-default", + "workingSpaceLUT": "linear", + "monitorLut": "sRGB", + "int8Lut": "sRGB", + "int16Lut": "sRGB", + "logLut": "Cineon", + "floatLut": "linear" + }, + "nodes": { + "requiredNodes": [ + { + "plugins": [ + "CreateWriteRender" + ], + "nukeNodeClass": "Write", + "knobs": [ + { + "type": "text", + "name": "file_type", + "text": "exr" + }, + { + "type": "text", + "name": "datatype", + "text": "16 bit half" + }, + { + "type": "text", + "name": "compression", + "text": "Zip (1 scanline)" + }, + { + "type": "boolean", + "name": "autocrop", + "boolean": True + }, + { + "type": "color_gui", + "name": "tile_color", + "color_gui": [ + 186, + 35, + 35 + ] + }, + { + "type": "text", + "name": "channels", + "text": "rgb" + }, + { + "type": "text", + "name": "colorspace", + "text": "linear" + }, + { + "type": "boolean", + "name": "create_directories", + "boolean": True + } + ] + }, + { + "plugins": [ + "CreateWritePrerender" + ], + "nukeNodeClass": "Write", + "knobs": [ + { + "type": "text", + "name": "file_type", + "text": "exr" + }, + { + "type": "text", + "name": "datatype", + "text": "16 bit half" + }, + { + "type": "text", + "name": "compression", + "text": "Zip (1 scanline)" + }, + { + "type": "boolean", + "name": "autocrop", + "boolean": True + }, + { + "type": "color_gui", + "name": "tile_color", + "color_gui": [ + 171, + 171, + 10 + ] + }, + { + "type": "text", + "name": "channels", + "text": "rgb" + }, + { + "type": "text", + "name": "colorspace", + "text": "linear" + }, + { + "type": "boolean", + "name": "create_directories", + "boolean": True + } + ] + }, + { + "plugins": [ + "CreateWriteImage" + ], + "nukeNodeClass": "Write", + "knobs": [ + { + "type": "text", + "name": "file_type", + "text": "tiff" + }, + { + "type": "text", + "name": "datatype", + "text": "16 bit" + }, + { + "type": "text", + "name": "compression", + "text": "Deflate" + }, + { + "type": "color_gui", + "name": "tile_color", + "color_gui": [ + 56, + 162, + 7 + ] + }, + { + "type": "text", + "name": "channels", + "text": "rgb" + }, + { + "type": "text", + "name": "colorspace", + "text": "sRGB" + }, + { + "type": "boolean", + "name": "create_directories", + "boolean": True + } + ] + } + ], + "overrideNodes": [] + }, + "regexInputs": { + "inputs": [ + { + "regex": "(beauty).*(?=.exr)", + "colorspace": "linear" + } + ] + } +} diff --git a/server_addon/nuke/server/settings/loader_plugins.py b/server_addon/nuke/server/settings/loader_plugins.py new file mode 100644 index 0000000000..6db381bffb --- /dev/null +++ b/server_addon/nuke/server/settings/loader_plugins.py @@ -0,0 +1,80 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class LoadImageModel(BaseSettingsModel): + enabled: bool = Field( + title="Enabled" + ) + """# TODO: v3 api used `_representation` + New api is hiding it so it had to be renamed + to `representations_include` + """ + representations_include: list[str] = Field( + default_factory=list, + title="Include representations" + ) + + node_name_template: str = Field( + title="Read node name template" + ) + + +class LoadClipOptionsModel(BaseSettingsModel): + start_at_workfile: bool = Field( + title="Start at workfile's start frame" + ) + add_retime: bool = Field( + title="Add retime" + ) + + +class LoadClipModel(BaseSettingsModel): + enabled: bool = Field( + title="Enabled" + ) + """# TODO: v3 api used `_representation` + New api is hiding it so it had to be renamed + to `representations_include` + """ + representations_include: list[str] = Field( + default_factory=list, + title="Include representations" + ) + + node_name_template: str = Field( + title="Read node name template" + ) + options_defaults: LoadClipOptionsModel = Field( + default_factory=LoadClipOptionsModel, + title="Loader option defaults" + ) + + +class LoaderPuginsModel(BaseSettingsModel): + LoadImage: LoadImageModel = Field( + default_factory=LoadImageModel, + title="Load Image" + ) + LoadClip: LoadClipModel = Field( + default_factory=LoadClipModel, + title="Load Clip" + ) + + +DEFAULT_LOADER_PLUGINS_SETTINGS = { + "LoadImage": { + "enabled": True, + "representations_include": [], + "node_name_template": "{class_name}_{ext}" + }, + "LoadClip": { + "enabled": True, + "representations_include": [], + "node_name_template": "{class_name}_{ext}", + "options_defaults": { + "start_at_workfile": True, + "add_retime": True + } + } +} diff --git a/server_addon/nuke/server/settings/main.py b/server_addon/nuke/server/settings/main.py new file mode 100644 index 0000000000..4687d48ac9 --- /dev/null +++ b/server_addon/nuke/server/settings/main.py @@ -0,0 +1,128 @@ +from pydantic import validator, Field + +from ayon_server.settings import ( + BaseSettingsModel, + ensure_unique_names +) + +from .general import ( + GeneralSettings, + DEFAULT_GENERAL_SETTINGS +) +from .imageio import ( + ImageIOSettings, + DEFAULT_IMAGEIO_SETTINGS +) +from .dirmap import ( + DirmapSettings, + DEFAULT_DIRMAP_SETTINGS +) +from .scriptsmenu import ( + ScriptsmenuSettings, + DEFAULT_SCRIPTSMENU_SETTINGS +) +from .gizmo import ( + GizmoItem, + DEFAULT_GIZMO_ITEM +) +from .create_plugins import ( + CreatorPluginsSettings, + DEFAULT_CREATE_SETTINGS +) +from .publish_plugins import ( + PublishPuginsModel, + DEFAULT_PUBLISH_PLUGIN_SETTINGS +) +from .loader_plugins import ( + LoaderPuginsModel, + DEFAULT_LOADER_PLUGINS_SETTINGS +) +from .workfile_builder import ( + WorkfileBuilderModel, + DEFAULT_WORKFILE_BUILDER_SETTINGS +) +from .templated_workfile_build import ( + TemplatedWorkfileBuildModel +) +from .filters import PublishGUIFilterItemModel + + +class NukeSettings(BaseSettingsModel): + """Nuke addon settings.""" + + general: GeneralSettings = Field( + default_factory=GeneralSettings, + title="General", + ) + + imageio: ImageIOSettings = Field( + default_factory=ImageIOSettings, + title="Color Management (imageio)", + ) + """# TODO: fix host api: + - rename `nuke-dirmap` to `dirmap` was inevitable + """ + dirmap: DirmapSettings = Field( + default_factory=DirmapSettings, + title="Nuke Directory Mapping", + ) + + scriptsmenu: ScriptsmenuSettings = Field( + default_factory=ScriptsmenuSettings, + title="Scripts Menu Definition", + ) + + gizmo: list[GizmoItem] = Field( + default_factory=list, title="Gizmo Menu") + + create: CreatorPluginsSettings = Field( + default_factory=CreatorPluginsSettings, + title="Creator Plugins", + ) + + publish: PublishPuginsModel = Field( + default_factory=PublishPuginsModel, + title="Publish Plugins", + ) + + load: LoaderPuginsModel = Field( + default_factory=LoaderPuginsModel, + title="Loader Plugins", + ) + + workfile_builder: WorkfileBuilderModel = Field( + default_factory=WorkfileBuilderModel, + title="Workfile Builder", + ) + + templated_workfile_build: TemplatedWorkfileBuildModel = Field( + title="Templated Workfile Build", + default_factory=TemplatedWorkfileBuildModel + ) + + filters: list[PublishGUIFilterItemModel] = Field( + default_factory=list + ) + + @validator("filters") + def ensure_unique_names(cls, value): + """Ensure name fields within the lists have unique names.""" + ensure_unique_names(value) + return value + + +DEFAULT_VALUES = { + "general": DEFAULT_GENERAL_SETTINGS, + "imageio": DEFAULT_IMAGEIO_SETTINGS, + "dirmap": DEFAULT_DIRMAP_SETTINGS, + "scriptsmenu": DEFAULT_SCRIPTSMENU_SETTINGS, + "gizmo": [DEFAULT_GIZMO_ITEM], + "create": DEFAULT_CREATE_SETTINGS, + "publish": DEFAULT_PUBLISH_PLUGIN_SETTINGS, + "load": DEFAULT_LOADER_PLUGINS_SETTINGS, + "workfile_builder": DEFAULT_WORKFILE_BUILDER_SETTINGS, + "templated_workfile_build": { + "profiles": [] + }, + "filters": [] +} diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py new file mode 100644 index 0000000000..f057fd629d --- /dev/null +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -0,0 +1,536 @@ +from pydantic import validator, Field +from ayon_server.settings import ( + BaseSettingsModel, + ensure_unique_names, + task_types_enum +) +from .common import KnobModel, validate_json_dict + + +def nuke_render_publish_types_enum(): + """Return all nuke render families available in creators.""" + return [ + {"value": "render", "label": "Render"}, + {"value": "prerender", "label": "Prerender"}, + {"value": "image", "label": "Image"} + ] + + +def nuke_product_types_enum(): + """Return all nuke families available in creators.""" + return [ + {"value": "nukenodes", "label": "Nukenodes"}, + {"value": "model", "label": "Model"}, + {"value": "camera", "label": "Camera"}, + {"value": "gizmo", "label": "Gizmo"}, + {"value": "source", "label": "Source"} + ] + nuke_render_publish_types_enum() + + +class NodeModel(BaseSettingsModel): + # TODO: missing in host api + name: str = Field( + title="Node name" + ) + # TODO: `nodeclass` rename to `nuke_node_class` + nodeclass: str = Field( + "", + title="Node class" + ) + dependent: str = Field( + "", + title="Incoming dependency" + ) + """# TODO: Changes in host api: + - Need complete rework of knob types in nuke integration. + - We could not support v3 style of settings. + """ + knobs: list[KnobModel] = Field( + title="Knobs", + ) + + @validator("knobs") + def ensure_unique_names(cls, value): + """Ensure name fields within the lists have unique names.""" + ensure_unique_names(value) + return value + + +class ThumbnailRepositionNodeModel(BaseSettingsModel): + node_class: str = Field(title="Node class") + knobs: list[KnobModel] = Field(title="Knobs", default_factory=list) + + @validator("knobs") + def ensure_unique_names(cls, value): + """Ensure name fields within the lists have unique names.""" + ensure_unique_names(value) + return value + + +class CollectInstanceDataModel(BaseSettingsModel): + sync_workfile_version_on_product_types: list[str] = Field( + default_factory=list, + enum_resolver=nuke_product_types_enum, + title="Sync workfile versions for familes" + ) + + +class OptionalPluginModel(BaseSettingsModel): + enabled: bool = Field(True) + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + + +class ValidateKnobsModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + knobs: str = Field( + "{}", + title="Knobs", + widget="textarea", + ) + + @validator("knobs") + def validate_json(cls, value): + return validate_json_dict(value) + + +class ExtractThumbnailModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + use_rendered: bool = Field(title="Use rendered images") + bake_viewer_process: bool = Field(title="Bake view process") + bake_viewer_input_process: bool = Field(title="Bake viewer input process") + """# TODO: needs to rewrite from v3 to ayon + - `nodes` in v3 was dict but now `prenodes` is list of dict + - also later `nodes` should be `prenodes` + """ + + nodes: list[NodeModel] = Field( + title="Nodes (deprecated)" + ) + reposition_nodes: list[ThumbnailRepositionNodeModel] = Field( + title="Reposition nodes", + default_factory=list + ) + + +class ExtractReviewDataModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + + +class ExtractReviewDataLutModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + + +class BakingStreamFilterModel(BaseSettingsModel): + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + product_types: list[str] = Field( + default_factory=list, + enum_resolver=nuke_render_publish_types_enum, + title="Sync workfile versions for familes" + ) + product_names: list[str] = Field( + default_factory=list, title="Product names") + + +class ReformatNodesRepositionNodes(BaseSettingsModel): + node_class: str = Field(title="Node class") + knobs: list[KnobModel] = Field( + default_factory=list, + title="Node knobs") + + +class ReformatNodesConfigModel(BaseSettingsModel): + """Only reposition nodes supported. + + You can add multiple reformat nodes and set their knobs. + Order of reformat nodes is important. First reformat node will + be applied first and last reformat node will be applied last. + """ + enabled: bool = Field(False) + reposition_nodes: list[ReformatNodesRepositionNodes] = Field( + default_factory=list, + title="Reposition knobs" + ) + + +class BakingStreamModel(BaseSettingsModel): + name: str = Field(title="Output name") + filter: BakingStreamFilterModel = Field( + title="Filter", default_factory=BakingStreamFilterModel) + read_raw: bool = Field(title="Read raw switch") + viewer_process_override: str = Field(title="Viewer process override") + bake_viewer_process: bool = Field(title="Bake view process") + bake_viewer_input_process: bool = Field(title="Bake viewer input process") + reformat_node_add: bool = Field(title="Add reformat node") + reformat_node_config: list[KnobModel] = Field( + default_factory=list, + title="Reformat node properties") + reformat_nodes_config: ReformatNodesConfigModel = Field( + default_factory=ReformatNodesConfigModel, + title="Reformat Nodes") + extension: str = Field(title="File extension") + add_custom_tags: list[str] = Field( + title="Custom tags", default_factory=list) + + +class ExtractReviewDataMovModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + viewer_lut_raw: bool = Field(title="Viewer lut raw") + outputs: list[BakingStreamModel] = Field( + title="Baking streams" + ) + + +class FSubmissionNoteModel(BaseSettingsModel): + enabled: bool = Field(title="enabled") + template: str = Field(title="Template") + + +class FSubmistingForModel(BaseSettingsModel): + enabled: bool = Field(title="enabled") + template: str = Field(title="Template") + + +class FVFXScopeOfWorkModel(BaseSettingsModel): + enabled: bool = Field(title="enabled") + template: str = Field(title="Template") + + +class ExctractSlateFrameParamModel(BaseSettingsModel): + f_submission_note: FSubmissionNoteModel = Field( + title="f_submission_note", + default_factory=FSubmissionNoteModel + ) + f_submitting_for: FSubmistingForModel = Field( + title="f_submitting_for", + default_factory=FSubmistingForModel + ) + f_vfx_scope_of_work: FVFXScopeOfWorkModel = Field( + title="f_vfx_scope_of_work", + default_factory=FVFXScopeOfWorkModel + ) + + +class ExtractSlateFrameModel(BaseSettingsModel): + viewer_lut_raw: bool = Field(title="Viewer lut raw") + """# TODO: v3 api different model: + - not possible to replicate v3 model: + {"name": [bool, str]} + - not it is: + {"name": {"enabled": bool, "template": str}} + """ + key_value_mapping: ExctractSlateFrameParamModel = Field( + title="Key value mapping", + default_factory=ExctractSlateFrameParamModel + ) + + +class IncrementScriptVersionModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + + +class PublishPuginsModel(BaseSettingsModel): + CollectInstanceData: CollectInstanceDataModel = Field( + title="Collect Instance Version", + default_factory=CollectInstanceDataModel, + section="Collectors" + ) + ValidateCorrectAssetName: OptionalPluginModel = Field( + title="Validate Correct Folder Name", + default_factory=OptionalPluginModel, + section="Validators" + ) + ValidateContainers: OptionalPluginModel = Field( + title="Validate Containers", + default_factory=OptionalPluginModel + ) + ValidateKnobs: ValidateKnobsModel = Field( + title="Validate Knobs", + default_factory=ValidateKnobsModel + ) + ValidateOutputResolution: OptionalPluginModel = Field( + title="Validate Output Resolution", + default_factory=OptionalPluginModel + ) + ValidateGizmo: OptionalPluginModel = Field( + title="Validate Gizmo", + default_factory=OptionalPluginModel + ) + ValidateBackdrop: OptionalPluginModel = Field( + title="Validate Backdrop", + default_factory=OptionalPluginModel + ) + ValidateScript: OptionalPluginModel = Field( + title="Validate Script", + default_factory=OptionalPluginModel + ) + ExtractThumbnail: ExtractThumbnailModel = Field( + title="Extract Thumbnail", + default_factory=ExtractThumbnailModel, + section="Extractors" + ) + ExtractReviewData: ExtractReviewDataModel = Field( + title="Extract Review Data", + default_factory=ExtractReviewDataModel + ) + ExtractReviewDataLut: ExtractReviewDataLutModel = Field( + title="Extract Review Data Lut", + default_factory=ExtractReviewDataLutModel + ) + ExtractReviewDataMov: ExtractReviewDataMovModel = Field( + title="Extract Review Data Mov", + default_factory=ExtractReviewDataMovModel + ) + ExtractSlateFrame: ExtractSlateFrameModel = Field( + title="Extract Slate Frame", + default_factory=ExtractSlateFrameModel + ) + # TODO: plugin should be renamed - `workfile` not `script` + IncrementScriptVersion: IncrementScriptVersionModel = Field( + title="Increment Workfile Version", + default_factory=IncrementScriptVersionModel, + section="Integrators" + ) + + +DEFAULT_PUBLISH_PLUGIN_SETTINGS = { + "CollectInstanceData": { + "sync_workfile_version_on_product_types": [ + "nukenodes", + "camera", + "gizmo", + "source", + "render", + "write" + ] + }, + "ValidateCorrectAssetName": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateContainers": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateKnobs": { + "enabled": False, + "knobs": "\n".join([ + '{', + ' "render": {', + ' "review": true', + ' }', + '}' + ]) + }, + "ValidateOutputResolution": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateGizmo": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateBackdrop": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateScript": { + "enabled": True, + "optional": True, + "active": True + }, + "ExtractThumbnail": { + "enabled": True, + "use_rendered": True, + "bake_viewer_process": True, + "bake_viewer_input_process": True, + "nodes": [ + { + "name": "Reformat01", + "nodeclass": "Reformat", + "dependency": "", + "knobs": [ + { + "type": "text", + "name": "type", + "text": "to format" + }, + { + "type": "text", + "name": "format", + "text": "HD_1080" + }, + { + "type": "text", + "name": "filter", + "text": "Lanczos6" + }, + { + "type": "boolean", + "name": "black_outside", + "boolean": True + }, + { + "type": "boolean", + "name": "pbb", + "boolean": False + } + ] + } + ], + "reposition_nodes": [ + { + "node_class": "Reformat", + "knobs": [ + { + "type": "text", + "name": "type", + "text": "to format" + }, + { + "type": "text", + "name": "format", + "text": "HD_1080" + }, + { + "type": "text", + "name": "filter", + "text": "Lanczos6" + }, + { + "type": "bool", + "name": "black_outside", + "boolean": True + }, + { + "type": "bool", + "name": "pbb", + "boolean": False + } + ] + } + ] + }, + "ExtractReviewData": { + "enabled": False + }, + "ExtractReviewDataLut": { + "enabled": False + }, + "ExtractReviewDataMov": { + "enabled": True, + "viewer_lut_raw": False, + "outputs": [ + { + "name": "baking", + "filter": { + "task_types": [], + "product_types": [], + "product_names": [] + }, + "read_raw": False, + "viewer_process_override": "", + "bake_viewer_process": True, + "bake_viewer_input_process": True, + "reformat_node_add": False, + "reformat_node_config": [ + { + "type": "text", + "name": "type", + "text": "to format" + }, + { + "type": "text", + "name": "format", + "text": "HD_1080" + }, + { + "type": "text", + "name": "filter", + "text": "Lanczos6" + }, + { + "type": "boolean", + "name": "black_outside", + "boolean": True + }, + { + "type": "boolean", + "name": "pbb", + "boolean": False + } + ], + "reformat_nodes_config": { + "enabled": False, + "reposition_nodes": [ + { + "node_class": "Reformat", + "knobs": [ + { + "type": "text", + "name": "type", + "text": "to format" + }, + { + "type": "text", + "name": "format", + "text": "HD_1080" + }, + { + "type": "text", + "name": "filter", + "text": "Lanczos6" + }, + { + "type": "bool", + "name": "black_outside", + "boolean": True + }, + { + "type": "bool", + "name": "pbb", + "boolean": False + } + ] + } + ] + }, + "extension": "mov", + "add_custom_tags": [] + } + ] + }, + "ExtractSlateFrame": { + "viewer_lut_raw": False, + "key_value_mapping": { + "f_submission_note": { + "enabled": True, + "template": "{comment}" + }, + "f_submitting_for": { + "enabled": True, + "template": "{intent[value]}" + }, + "f_vfx_scope_of_work": { + "enabled": False, + "template": "" + } + } + }, + "IncrementScriptVersion": { + "enabled": True, + "optional": True, + "active": True + } +} diff --git a/server_addon/nuke/server/settings/scriptsmenu.py b/server_addon/nuke/server/settings/scriptsmenu.py new file mode 100644 index 0000000000..9d1c32ebac --- /dev/null +++ b/server_addon/nuke/server/settings/scriptsmenu.py @@ -0,0 +1,54 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class ScriptsmenuSubmodel(BaseSettingsModel): + """Item Definition""" + _isGroup = True + + type: str = Field(title="Type") + command: str = Field(title="Command") + sourcetype: str = Field(title="Source Type") + title: str = Field(title="Title") + tooltip: str = Field(title="Tooltip") + + +class ScriptsmenuSettings(BaseSettingsModel): + """Nuke script menu project settings.""" + _isGroup = True + + # TODO: in api rename key `name` to `menu_name` + name: str = Field(title="Menu Name") + definition: list[ScriptsmenuSubmodel] = Field( + default_factory=list, + title="Definition", + description="Scriptmenu Items Definition" + ) + + +DEFAULT_SCRIPTSMENU_SETTINGS = { + "name": "OpenPype Tools", + "definition": [ + { + "type": "action", + "sourcetype": "python", + "title": "OpenPype Docs", + "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')", + "tooltip": "Open the OpenPype Nuke user doc page" + }, + { + "type": "action", + "sourcetype": "python", + "title": "Set Frame Start (Read Node)", + "command": "from openpype.hosts.nuke.startup.frame_setting_for_read_nodes import main;main();", + "tooltip": "Set frame start for read node(s)" + }, + { + "type": "action", + "sourcetype": "python", + "title": "Set non publish output for Write Node", + "command": "from openpype.hosts.nuke.startup.custom_write_node import main;main();", + "tooltip": "Open the OpenPype Nuke user doc page" + } + ] +} diff --git a/server_addon/nuke/server/settings/templated_workfile_build.py b/server_addon/nuke/server/settings/templated_workfile_build.py new file mode 100644 index 0000000000..e0245c8d06 --- /dev/null +++ b/server_addon/nuke/server/settings/templated_workfile_build.py @@ -0,0 +1,33 @@ +from pydantic import Field +from ayon_server.settings import ( + BaseSettingsModel, + task_types_enum, +) + + +class TemplatedWorkfileProfileModel(BaseSettingsModel): + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field( + default_factory=list, + title="Task names" + ) + path: str = Field( + title="Path to template" + ) + keep_placeholder: bool = Field( + False, + title="Keep placeholders") + create_first_version: bool = Field( + True, + title="Create first version" + ) + + +class TemplatedWorkfileBuildModel(BaseSettingsModel): + profiles: list[TemplatedWorkfileProfileModel] = Field( + default_factory=list + ) diff --git a/server_addon/nuke/server/settings/workfile_builder.py b/server_addon/nuke/server/settings/workfile_builder.py new file mode 100644 index 0000000000..ee67c7c16a --- /dev/null +++ b/server_addon/nuke/server/settings/workfile_builder.py @@ -0,0 +1,72 @@ +from pydantic import Field +from ayon_server.settings import ( + BaseSettingsModel, + task_types_enum, + MultiplatformPathModel, +) + + +class CustomTemplateModel(BaseSettingsModel): + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + path: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Gizmo Directory Path" + ) + + +class BuilderProfileItemModel(BaseSettingsModel): + product_name_filters: list[str] = Field( + default_factory=list, + title="Product name" + ) + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + repre_names: list[str] = Field( + default_factory=list, + title="Representations" + ) + loaders: list[str] = Field( + default_factory=list, + title="Loader plugins" + ) + + +class BuilderProfileModel(BaseSettingsModel): + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + tasks: list[str] = Field( + default_factory=list, + title="Task names" + ) + current_context: list[BuilderProfileItemModel] = Field( + title="Current context") + linked_assets: list[BuilderProfileItemModel] = Field( + title="Linked assets/shots") + + +class WorkfileBuilderModel(BaseSettingsModel): + create_first_version: bool = Field( + title="Create first workfile") + custom_templates: list[CustomTemplateModel] = Field( + title="Custom templates") + builder_on_start: bool = Field( + title="Run Builder at first workfile") + profiles: list[BuilderProfileModel] = Field( + title="Builder profiles") + + +DEFAULT_WORKFILE_BUILDER_SETTINGS = { + "create_first_version": False, + "custom_templates": [], + "builder_on_start": False, + "profiles": [] +} diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/nuke/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/client/pyproject.toml b/server_addon/openpype/client/pyproject.toml similarity index 100% rename from server_addon/client/pyproject.toml rename to server_addon/openpype/client/pyproject.toml diff --git a/server_addon/server/__init__.py b/server_addon/openpype/server/__init__.py similarity index 100% rename from server_addon/server/__init__.py rename to server_addon/openpype/server/__init__.py diff --git a/server_addon/photoshop/LICENSE b/server_addon/photoshop/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/server_addon/photoshop/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/server_addon/photoshop/README.md b/server_addon/photoshop/README.md new file mode 100644 index 0000000000..2d1e1c745c --- /dev/null +++ b/server_addon/photoshop/README.md @@ -0,0 +1,4 @@ +Photoshp Addon +=============== + +Integration with Adobe Photoshop. diff --git a/server_addon/photoshop/server/__init__.py b/server_addon/photoshop/server/__init__.py new file mode 100644 index 0000000000..e7ac218b5a --- /dev/null +++ b/server_addon/photoshop/server/__init__.py @@ -0,0 +1,15 @@ +from ayon_server.addons import BaseServerAddon + +from .settings import PhotoshopSettings, DEFAULT_PHOTOSHOP_SETTING +from .version import __version__ + + +class Photoshop(BaseServerAddon): + name = "photoshop" + version = __version__ + + settings_model = PhotoshopSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_PHOTOSHOP_SETTING) diff --git a/server_addon/photoshop/server/settings/__init__.py b/server_addon/photoshop/server/settings/__init__.py new file mode 100644 index 0000000000..9ae5764362 --- /dev/null +++ b/server_addon/photoshop/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + PhotoshopSettings, + DEFAULT_PHOTOSHOP_SETTING, +) + + +__all__ = ( + "PhotoshopSettings", + "DEFAULT_PHOTOSHOP_SETTING", +) diff --git a/server_addon/photoshop/server/settings/creator_plugins.py b/server_addon/photoshop/server/settings/creator_plugins.py new file mode 100644 index 0000000000..2fe63a7e3a --- /dev/null +++ b/server_addon/photoshop/server/settings/creator_plugins.py @@ -0,0 +1,79 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class CreateImagePluginModel(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + active_on_create: bool = Field(True, title="Active by default") + mark_for_review: bool = Field(False, title="Review by default") + default_variants: list[str] = Field( + default_factory=list, + title="Default Variants" + ) + + +class AutoImageCreatorPluginModel(BaseSettingsModel): + enabled: bool = Field(False, title="Enabled") + active_on_create: bool = Field(True, title="Active by default") + mark_for_review: bool = Field(False, title="Review by default") + default_variant: str = Field("", title="Default Variants") + + +class CreateReviewPlugin(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + active_on_create: bool = Field(True, title="Active by default") + default_variant: str = Field("", title="Default Variants") + + +class CreateWorkfilelugin(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + active_on_create: bool = Field(True, title="Active by default") + default_variant: str = Field("", title="Default Variants") + + +class PhotoshopCreatorPlugins(BaseSettingsModel): + ImageCreator: CreateImagePluginModel = Field( + title="Create Image", + default_factory=CreateImagePluginModel, + ) + AutoImageCreator: AutoImageCreatorPluginModel = Field( + title="Create Flatten Image", + default_factory=AutoImageCreatorPluginModel, + ) + ReviewCreator: CreateReviewPlugin = Field( + title="Create Review", + default_factory=CreateReviewPlugin, + ) + WorkfileCreator: CreateWorkfilelugin = Field( + title="Create Workfile", + default_factory=CreateWorkfilelugin, + ) + + +DEFAULT_CREATE_SETTINGS = { + "ImageCreator": { + "enabled": True, + "active_on_create": True, + "mark_for_review": False, + "default_variants": [ + "Main" + ] + }, + "AutoImageCreator": { + "enabled": False, + "active_on_create": True, + "mark_for_review": False, + "default_variant": "" + }, + "ReviewCreator": { + "enabled": True, + "active_on_create": True, + "default_variant": "" + }, + "WorkfileCreator": { + "enabled": True, + "active_on_create": True, + "default_variant": "Main" + } +} diff --git a/server_addon/photoshop/server/settings/imageio.py b/server_addon/photoshop/server/settings/imageio.py new file mode 100644 index 0000000000..56b7f2fa32 --- /dev/null +++ b/server_addon/photoshop/server/settings/imageio.py @@ -0,0 +1,64 @@ +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 ImageIORemappingRulesModel(BaseSettingsModel): + host_native_name: str = Field( + title="Application native colorspace name" + ) + ocio_name: str = Field(title="OCIO colorspace name") + + +class ImageIORemappingModel(BaseSettingsModel): + rules: list[ImageIORemappingRulesModel] = Field( + default_factory=list) + + +class PhotoshopImageIOModel(BaseSettingsModel): + activate_host_color_management: bool = Field( + True, title="Enable Color Management" + ) + remapping: ImageIORemappingModel = Field( + title="Remapping colorspace names", + default_factory=ImageIORemappingModel + ) + ocio_config: ImageIOConfigModel = Field( + default_factory=ImageIOConfigModel, + title="OCIO config" + ) + file_rules: ImageIOFileRulesModel = Field( + default_factory=ImageIOFileRulesModel, + title="File Rules" + ) diff --git a/server_addon/photoshop/server/settings/main.py b/server_addon/photoshop/server/settings/main.py new file mode 100644 index 0000000000..ae7705b3db --- /dev/null +++ b/server_addon/photoshop/server/settings/main.py @@ -0,0 +1,41 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + +from .imageio import PhotoshopImageIOModel +from .creator_plugins import PhotoshopCreatorPlugins, DEFAULT_CREATE_SETTINGS +from .publish_plugins import PhotoshopPublishPlugins, DEFAULT_PUBLISH_SETTINGS +from .workfile_builder import WorkfileBuilderPlugin + + +class PhotoshopSettings(BaseSettingsModel): + """Photoshop Project Settings.""" + + imageio: PhotoshopImageIOModel = Field( + default_factory=PhotoshopImageIOModel, + title="OCIO config" + ) + + create: PhotoshopCreatorPlugins = Field( + default_factory=PhotoshopCreatorPlugins, + title="Creator plugins" + ) + + publish: PhotoshopPublishPlugins = Field( + default_factory=PhotoshopPublishPlugins, + title="Publish plugins" + ) + + workfile_builder: WorkfileBuilderPlugin = Field( + default_factory=WorkfileBuilderPlugin, + title="Workfile Builder" + ) + + +DEFAULT_PHOTOSHOP_SETTING = { + "create": DEFAULT_CREATE_SETTINGS, + "publish": DEFAULT_PUBLISH_SETTINGS, + "workfile_builder": { + "create_first_version": False, + "custom_templates": [] + } +} diff --git a/server_addon/photoshop/server/settings/publish_plugins.py b/server_addon/photoshop/server/settings/publish_plugins.py new file mode 100644 index 0000000000..6bc72b4072 --- /dev/null +++ b/server_addon/photoshop/server/settings/publish_plugins.py @@ -0,0 +1,221 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +create_flatten_image_enum = [ + {"value": "flatten_with_images", "label": "Flatten with images"}, + {"value": "flatten_only", "label": "Flatten only"}, + {"value": "no", "label": "No"}, +] + + +color_code_enum = [ + {"value": "red", "label": "Red"}, + {"value": "orange", "label": "Orange"}, + {"value": "yellowColor", "label": "Yellow"}, + {"value": "grain", "label": "Green"}, + {"value": "blue", "label": "Blue"}, + {"value": "violet", "label": "Violet"}, + {"value": "gray", "label": "Gray"}, +] + + +class ColorCodeMappings(BaseSettingsModel): + color_code: list[str] = Field( + title="Color codes for layers", + default_factory=list, + enum_resolver=lambda: color_code_enum, + ) + + layer_name_regex: list[str] = Field( + "", + title="Layer name regex" + ) + + product_type: str = Field( + "", + title="Resulting product type" + ) + + product_name_template: str = Field( + "", + title="Product name template" + ) + + +class ExtractedOptions(BaseSettingsModel): + tags: list[str] = Field( + title="Tags", + default_factory=list + ) + + +class CollectColorCodedInstancesPlugin(BaseSettingsModel): + """Set color for publishable layers, set its resulting product type + and template for product name. \n Can create flatten image from published + instances. + (Applicable only for remote publishing!)""" + + enabled: bool = Field(True, title="Enabled") + create_flatten_image: str = Field( + "", + title="Create flatten image", + enum_resolver=lambda: create_flatten_image_enum, + ) + + flatten_product_type_template: str = Field( + "", + title="Subset template for flatten image" + ) + + color_code_mapping: list[ColorCodeMappings] = Field( + title="Color code mappings", + default_factory=ColorCodeMappings, + ) + + +class CollectReviewPlugin(BaseSettingsModel): + """Should review product be created""" + enabled: bool = Field(True, title="Enabled") + + +class CollectVersionPlugin(BaseSettingsModel): + """Synchronize version for image and review instances by workfile version""" # noqa + enabled: bool = Field(True, title="Enabled") + + +class ValidateContainersPlugin(BaseSettingsModel): + """Check that workfile contains latest version of loaded items""" # noqa + _isGroup = True + enabled: bool = True + optional: bool = Field(False, title="Optional") + active: bool = Field(True, title="Active") + + +class ValidateNamingPlugin(BaseSettingsModel): + """Validate naming of products and layers""" # noqa + invalid_chars: str = Field( + '', + title="Regex pattern of invalid characters" + ) + + replace_char: str = Field( + '', + title="Replacement character" + ) + + +class ExtractImagePlugin(BaseSettingsModel): + """Currently only jpg and png are supported""" + formats: list[str] = Field( + title="Extract Formats", + default_factory=list, + ) + + +class ExtractReviewPlugin(BaseSettingsModel): + make_image_sequence: bool = Field( + False, + title="Make an image sequence instead of flatten image" + ) + + max_downscale_size: int = Field( + 8192, + title="Maximum size of sources for review", + description="FFMpeg can only handle limited resolution for creation of review and/or thumbnail", # noqa + gt=300, # greater than + le=16384, # less or equal + ) + + jpg_options: ExtractedOptions = Field( + title="Extracted jpg Options", + default_factory=ExtractedOptions + ) + + mov_options: ExtractedOptions = Field( + title="Extracted mov Options", + default_factory=ExtractedOptions + ) + + +class PhotoshopPublishPlugins(BaseSettingsModel): + CollectColorCodedInstances: CollectColorCodedInstancesPlugin = Field( + title="Collect Color Coded Instances", + default_factory=CollectColorCodedInstancesPlugin, + ) + CollectReview: CollectReviewPlugin = Field( + title="Collect Review", + default_factory=CollectReviewPlugin, + ) + + CollectVersion: CollectVersionPlugin = Field( + title="Create Image", + default_factory=CollectVersionPlugin, + ) + + ValidateContainers: ValidateContainersPlugin = Field( + title="Validate Containers", + default_factory=ValidateContainersPlugin, + ) + + ValidateNaming: ValidateNamingPlugin = Field( + title="Validate naming of products and layers", + default_factory=ValidateNamingPlugin, + ) + + ExtractImage: ExtractImagePlugin = Field( + title="Extract Image", + default_factory=ExtractImagePlugin, + ) + + ExtractReview: ExtractReviewPlugin = Field( + title="Extract Review", + default_factory=ExtractReviewPlugin, + ) + + +DEFAULT_PUBLISH_SETTINGS = { + "CollectColorCodedInstances": { + "create_flatten_image": "no", + "flatten_product_type_template": "", + "color_code_mapping": [] + }, + "CollectReview": { + "enabled": True + }, + "CollectVersion": { + "enabled": False + }, + "ValidateContainers": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateNaming": { + "invalid_chars": "[ \\\\/+\\*\\?\\(\\)\\[\\]\\{\\}:,;]", + "replace_char": "_" + }, + "ExtractImage": { + "formats": [ + "png", + "jpg" + ] + }, + "ExtractReview": { + "make_image_sequence": False, + "max_downscale_size": 8192, + "jpg_options": { + "tags": [ + "review", + "ftrackreview" + ] + }, + "mov_options": { + "tags": [ + "review", + "ftrackreview" + ] + } + } +} diff --git a/server_addon/photoshop/server/settings/workfile_builder.py b/server_addon/photoshop/server/settings/workfile_builder.py new file mode 100644 index 0000000000..ec2ee136ad --- /dev/null +++ b/server_addon/photoshop/server/settings/workfile_builder.py @@ -0,0 +1,41 @@ +from pydantic import Field +from pathlib import Path + +from ayon_server.settings import BaseSettingsModel + + +class PathsTemplate(BaseSettingsModel): + windows: Path = Field( + '', + title="Windows" + ) + darwin: Path = Field( + '', + title="MacOS" + ) + linux: Path = Field( + '', + title="Linux" + ) + + +class CustomBuilderTemplate(BaseSettingsModel): + task_types: list[str] = Field( + default_factory=list, + title="Task types", + ) + template_path: PathsTemplate = Field( + default_factory=PathsTemplate + ) + + +class WorkfileBuilderPlugin(BaseSettingsModel): + _title = "Workfile Builder" + create_first_version: bool = Field( + False, + title="Create first workfile" + ) + + custom_templates: list[CustomBuilderTemplate] = Field( + default_factory=CustomBuilderTemplate + ) diff --git a/server_addon/photoshop/server/version.py b/server_addon/photoshop/server/version.py new file mode 100644 index 0000000000..d4b9e2d7f3 --- /dev/null +++ b/server_addon/photoshop/server/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Package declaring addon version.""" +__version__ = "0.1.0" diff --git a/server_addon/resolve/server/__init__.py b/server_addon/resolve/server/__init__.py new file mode 100644 index 0000000000..a84180d0f5 --- /dev/null +++ b/server_addon/resolve/server/__init__.py @@ -0,0 +1,19 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import ResolveSettings, DEFAULT_VALUES + + +class ResolveAddon(BaseServerAddon): + name = "resolve" + title = "DaVinci Resolve" + version = __version__ + settings_model: Type[ResolveSettings] = ResolveSettings + frontend_scopes = {} + services = {} + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/resolve/server/imageio.py b/server_addon/resolve/server/imageio.py new file mode 100644 index 0000000000..c2bfcd40d0 --- /dev/null +++ b/server_addon/resolve/server/imageio.py @@ -0,0 +1,64 @@ +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 ImageIORemappingRulesModel(BaseSettingsModel): + host_native_name: str = Field( + title="Application native colorspace name" + ) + ocio_name: str = Field(title="OCIO colorspace name") + + +class ImageIORemappingModel(BaseSettingsModel): + rules: list[ImageIORemappingRulesModel] = Field( + default_factory=list) + + +class ResolveImageIOModel(BaseSettingsModel): + activate_host_color_management: bool = Field( + True, title="Enable Color Management" + ) + remapping: ImageIORemappingModel = Field( + title="Remapping colorspace names", + default_factory=ImageIORemappingModel + ) + ocio_config: ImageIOConfigModel = Field( + default_factory=ImageIOConfigModel, + title="OCIO config" + ) + file_rules: ImageIOFileRulesModel = Field( + default_factory=ImageIOFileRulesModel, + title="File Rules" + ) diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py new file mode 100644 index 0000000000..326f6bea1e --- /dev/null +++ b/server_addon/resolve/server/settings.py @@ -0,0 +1,114 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + +from .imageio import ResolveImageIOModel + + +class CreateShotClipModels(BaseSettingsModel): + hierarchy: str = Field( + "{folder}/{sequence}", + title="Shot parent hierarchy", + section="Shot Hierarchy And Rename Settings" + ) + clipRename: bool = Field( + True, + title="Rename clips" + ) + clipName: str = Field( + "{track}{sequence}{shot}", + title="Clip name template" + ) + countFrom: int = Field( + 10, + title="Count sequence from" + ) + countSteps: int = Field( + 10, + title="Stepping number" + ) + + folder: str = Field( + "shots", + title="{folder}", + section="Shot Template Keywords" + ) + episode: str = Field( + "ep01", + title="{episode}" + ) + sequence: str = Field( + "sq01", + title="{sequence}" + ) + track: str = Field( + "{_track_}", + title="{track}" + ) + shot: str = Field( + "sh###", + title="{shot}" + ) + + vSyncOn: bool = Field( + False, + title="Enable Vertical Sync", + section="Vertical Synchronization Of Attributes" + ) + + workfileFrameStart: int = Field( + 1001, + title="Workfiles Start Frame", + section="Shot Attributes" + ) + handleStart: int = Field( + 10, + title="Handle start (head)" + ) + handleEnd: int = Field( + 10, + title="Handle end (tail)" + ) + + +class CreatorPuginsModel(BaseSettingsModel): + CreateShotClip: CreateShotClipModels = Field( + default_factory=CreateShotClipModels, + title="Create Shot Clip" + ) + + +class ResolveSettings(BaseSettingsModel): + launch_openpype_menu_on_start: bool = Field( + False, title="Launch OpenPype menu on start of Resolve" + ) + imageio: ResolveImageIOModel = Field( + default_factory=ResolveImageIOModel, + title="Color Management (ImageIO)" + ) + create: CreatorPuginsModel = Field( + default_factory=CreatorPuginsModel, + title="Creator plugins", + ) + + +DEFAULT_VALUES = { + "launch_openpype_menu_on_start": False, + "create": { + "CreateShotClip": { + "hierarchy": "{folder}/{sequence}", + "clipRename": True, + "clipName": "{track}{sequence}{shot}", + "countFrom": 10, + "countSteps": 10, + "folder": "shots", + "episode": "ep01", + "sequence": "sq01", + "track": "{_track_}", + "shot": "sh###", + "vSyncOn": False, + "workfileFrameStart": 1001, + "handleStart": 10, + "handleEnd": 10 + } + } +} diff --git a/server_addon/resolve/server/version.py b/server_addon/resolve/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/resolve/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/royal_render/server/__init__.py b/server_addon/royal_render/server/__init__.py new file mode 100644 index 0000000000..c5f0aafa00 --- /dev/null +++ b/server_addon/royal_render/server/__init__.py @@ -0,0 +1,17 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import RoyalRenderSettings, DEFAULT_VALUES + + +class RoyalRenderAddon(BaseServerAddon): + name = "royalrender" + version = __version__ + title = "Royal Render" + settings_model: Type[RoyalRenderSettings] = RoyalRenderSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/royal_render/server/settings.py b/server_addon/royal_render/server/settings.py new file mode 100644 index 0000000000..8b1fde6493 --- /dev/null +++ b/server_addon/royal_render/server/settings.py @@ -0,0 +1,53 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel, MultiplatformPathModel + + +class ServerListSubmodel(BaseSettingsModel): + _layout = "compact" + name: str = Field("", title="Name") + value: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel + ) + + +class CollectSequencesFromJobModel(BaseSettingsModel): + review: bool = Field(True, title="Generate reviews from sequences") + + +class PublishPluginsModel(BaseSettingsModel): + CollectSequencesFromJob: CollectSequencesFromJobModel = Field( + default_factory=CollectSequencesFromJobModel, + title="Collect Sequences from the Job" + ) + + +class RoyalRenderSettings(BaseSettingsModel): + enabled: bool = True + rr_paths: list[ServerListSubmodel] = Field( + default_factory=list, + title="Royal Render Root Paths", + ) + publish: PublishPluginsModel = Field( + default_factory=PublishPluginsModel, + title="Publish plugins" + ) + + +DEFAULT_VALUES = { + "enabled": False, + "rr_paths": [ + { + "name": "default", + "value": { + "windows": "", + "darwin": "", + "linux": "" + } + } + ], + "publish": { + "CollectSequencesFromJob": { + "review": True + } + } +} diff --git a/server_addon/royal_render/server/version.py b/server_addon/royal_render/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/royal_render/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/timers_manager/server/__init__.py b/server_addon/timers_manager/server/__init__.py new file mode 100644 index 0000000000..29f9d47370 --- /dev/null +++ b/server_addon/timers_manager/server/__init__.py @@ -0,0 +1,13 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import TimersManagerSettings + + +class TimersManagerAddon(BaseServerAddon): + name = "timers_manager" + version = __version__ + title = "Timers Manager" + settings_model: Type[TimersManagerSettings] = TimersManagerSettings diff --git a/server_addon/timers_manager/server/settings.py b/server_addon/timers_manager/server/settings.py new file mode 100644 index 0000000000..27dbc6ef8e --- /dev/null +++ b/server_addon/timers_manager/server/settings.py @@ -0,0 +1,9 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class TimersManagerSettings(BaseSettingsModel): + auto_stop: bool = Field(True, title="Auto stop timer") + full_time: int = Field(15, title="Max idle time") + message_time: float = Field(0.5, title="When dialog will show") + disregard_publishing: bool = Field(False, title="Disregard publishing") diff --git a/server_addon/timers_manager/server/version.py b/server_addon/timers_manager/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/timers_manager/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/traypublisher/server/LICENSE b/server_addon/traypublisher/server/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/server_addon/traypublisher/server/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/server_addon/traypublisher/server/README.md b/server_addon/traypublisher/server/README.md new file mode 100644 index 0000000000..c0029bc782 --- /dev/null +++ b/server_addon/traypublisher/server/README.md @@ -0,0 +1,4 @@ +Photoshp Addon +=============== + +Integration with Adobe Traypublisher. diff --git a/server_addon/traypublisher/server/__init__.py b/server_addon/traypublisher/server/__init__.py new file mode 100644 index 0000000000..308f32069f --- /dev/null +++ b/server_addon/traypublisher/server/__init__.py @@ -0,0 +1,15 @@ +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import TraypublisherSettings, DEFAULT_TRAYPUBLISHER_SETTING + + +class Traypublisher(BaseServerAddon): + name = "traypublisher" + version = __version__ + + settings_model = TraypublisherSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_TRAYPUBLISHER_SETTING) diff --git a/server_addon/traypublisher/server/settings/__init__.py b/server_addon/traypublisher/server/settings/__init__.py new file mode 100644 index 0000000000..bcf8beffa7 --- /dev/null +++ b/server_addon/traypublisher/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + TraypublisherSettings, + DEFAULT_TRAYPUBLISHER_SETTING, +) + + +__all__ = ( + "TraypublisherSettings", + "DEFAULT_TRAYPUBLISHER_SETTING", +) diff --git a/server_addon/traypublisher/server/settings/creator_plugins.py b/server_addon/traypublisher/server/settings/creator_plugins.py new file mode 100644 index 0000000000..345cb92e63 --- /dev/null +++ b/server_addon/traypublisher/server/settings/creator_plugins.py @@ -0,0 +1,46 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class BatchMovieCreatorPlugin(BaseSettingsModel): + """Allows to publish multiple video files in one go.
Name of matching + asset is parsed from file names ('asset.mov', 'asset_v001.mov', + 'my_asset_to_publish.mov')""" + + default_variants: list[str] = Field( + title="Default variants", + default_factory=list + ) + + default_tasks: list[str] = Field( + title="Default tasks", + default_factory=list + ) + + extensions: list[str] = Field( + title="Extensions", + default_factory=list + ) + + +class TrayPublisherCreatePluginsModel(BaseSettingsModel): + BatchMovieCreator: BatchMovieCreatorPlugin = Field( + title="Batch Movie Creator", + default_factory=BatchMovieCreatorPlugin + ) + + +DEFAULT_CREATORS = { + "BatchMovieCreator": { + "default_variants": [ + "Main" + ], + "default_tasks": [ + "Compositing" + ], + "extensions": [ + ".mov" + ] + }, +} diff --git a/server_addon/traypublisher/server/settings/editorial_creators.py b/server_addon/traypublisher/server/settings/editorial_creators.py new file mode 100644 index 0000000000..4111f22576 --- /dev/null +++ b/server_addon/traypublisher/server/settings/editorial_creators.py @@ -0,0 +1,181 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel, task_types_enum + + +class ClipNameTokenizerItem(BaseSettingsModel): + _layout = "expanded" + # TODO was 'dict-modifiable', is list of dicts now, must be fixed in code + name: str = Field("#TODO", title="Tokenizer name") + regex: str = Field("", title="Tokenizer regex") + + +class ShotAddTasksItem(BaseSettingsModel): + _layout = "expanded" + # TODO was 'dict-modifiable', is list of dicts now, must be fixed in code + name: str = Field('', title="Key") + task_type: list[str] = Field( + title="Task type", + default_factory=list, + enum_resolver=task_types_enum) + + +class ShotRenameSubmodel(BaseSettingsModel): + enabled: bool = True + shot_rename_template: str = Field( + "", + title="Shot rename template" + ) + + +parent_type_enum = [ + {"value": "Project", "label": "Project"}, + {"value": "Folder", "label": "Folder"}, + {"value": "Episode", "label": "Episode"}, + {"value": "Sequence", "label": "Sequence"}, +] + + +class TokenToParentConvertorItem(BaseSettingsModel): + # TODO - was 'type' must be renamed in code to `parent_type` + parent_type: str = Field( + "Project", + enum_resolver=lambda: parent_type_enum + ) + name: str = Field( + "", + title="Parent token name", + description="Unique name used in `Parent path template`" + ) + value: str = Field( + "", + title="Parent token value", + description="Template where any text, Anatomy keys and Tokens could be used" # noqa + ) + + +class ShotHierchySubmodel(BaseSettingsModel): + enabled: bool = True + parents_path: str = Field( + "", + title="Parents path template", + description="Using keys from \"Token to parent convertor\" or tokens directly" # noqa + ) + parents: list[TokenToParentConvertorItem] = Field( + default_factory=TokenToParentConvertorItem, + title="Token to parent convertor" + ) + + +output_file_type = [ + {"value": ".mp4", "label": "MP4"}, + {"value": ".mov", "label": "MOV"}, + {"value": ".wav", "label": "WAV"} +] + + +class ProductTypePresetItem(BaseSettingsModel): + product_type: str = Field("", title="Product type") + # TODO add placeholder '< Inherited >' + variant: str = Field("", title="Variant") + review: bool = Field(True, title="Review") + output_file_type: str = Field( + ".mp4", + enum_resolver=lambda: output_file_type + ) + + +class EditorialSimpleCreatorPlugin(BaseSettingsModel): + default_variants: list[str] = Field( + default_factory=list, + title="Default Variants" + ) + clip_name_tokenizer: list[ClipNameTokenizerItem] = Field( + default_factory=ClipNameTokenizerItem, + description=( + "Using Regex expression to create tokens. \nThose can be used" + " later in \"Shot rename\" creator \nor \"Shot hierarchy\"." + "\n\nTokens should be decorated with \"_\" on each side" + ) + ) + shot_rename: ShotRenameSubmodel = Field( + title="Shot Rename", + default_factory=ShotRenameSubmodel + ) + shot_hierarchy: ShotHierchySubmodel = Field( + title="Shot Hierarchy", + default_factory=ShotHierchySubmodel + ) + shot_add_tasks: list[ShotAddTasksItem] = Field( + title="Add tasks to shot", + default_factory=ShotAddTasksItem + ) + product_type_presets: list[ProductTypePresetItem] = Field( + default_factory=list + ) + + +class TraypublisherEditorialCreatorPlugins(BaseSettingsModel): + editorial_simple: EditorialSimpleCreatorPlugin = Field( + title="Editorial simple creator", + default_factory=EditorialSimpleCreatorPlugin, + ) + + +DEFAULT_EDITORIAL_CREATORS = { + "editorial_simple": { + "default_variants": [ + "Main" + ], + "clip_name_tokenizer": [ + {"name": "_sequence_", "regex": "(sc\\d{3})"}, + {"name": "_shot_", "regex": "(sh\\d{3})"} + ], + "shot_rename": { + "enabled": True, + "shot_rename_template": "{project[code]}_{_sequence_}_{_shot_}" + }, + "shot_hierarchy": { + "enabled": True, + "parents_path": "{project}/{folder}/{sequence}", + "parents": [ + { + "parent_type": "Project", + "name": "project", + "value": "{project[name]}" + }, + { + "parent_type": "Folder", + "name": "folder", + "value": "shots" + }, + { + "parent_type": "Sequence", + "name": "sequence", + "value": "{_sequence_}" + } + ] + }, + "shot_add_tasks": [], + "product_type_presets": [ + { + "product_type": "review", + "variant": "Reference", + "review": True, + "output_file_type": ".mp4" + }, + { + "product_type": "plate", + "variant": "", + "review": False, + "output_file_type": ".mov" + }, + { + "product_type": "audio", + "variant": "", + "review": False, + "output_file_type": ".wav" + } + ] + } +} diff --git a/server_addon/traypublisher/server/settings/imageio.py b/server_addon/traypublisher/server/settings/imageio.py new file mode 100644 index 0000000000..3df0d2f2fb --- /dev/null +++ b/server_addon/traypublisher/server/settings/imageio.py @@ -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 TrayPublisherImageIOModel(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" + ) diff --git a/server_addon/traypublisher/server/settings/main.py b/server_addon/traypublisher/server/settings/main.py new file mode 100644 index 0000000000..fad96bef2f --- /dev/null +++ b/server_addon/traypublisher/server/settings/main.py @@ -0,0 +1,52 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + +from .imageio import TrayPublisherImageIOModel +from .simple_creators import ( + SimpleCreatorPlugin, + DEFAULT_SIMPLE_CREATORS, +) +from .editorial_creators import ( + TraypublisherEditorialCreatorPlugins, + DEFAULT_EDITORIAL_CREATORS, +) +from .creator_plugins import ( + TrayPublisherCreatePluginsModel, + DEFAULT_CREATORS, +) +from .publish_plugins import ( + TrayPublisherPublishPlugins, + DEFAULT_PUBLISH_PLUGINS, +) + + +class TraypublisherSettings(BaseSettingsModel): + """Traypublisher Project Settings.""" + imageio: TrayPublisherImageIOModel = Field( + default_factory=TrayPublisherImageIOModel, + title="Color Management (ImageIO)" + ) + simple_creators: list[SimpleCreatorPlugin] = Field( + title="Simple Create Plugins", + default_factory=SimpleCreatorPlugin, + ) + editorial_creators: TraypublisherEditorialCreatorPlugins = Field( + title="Editorial Creators", + default_factory=TraypublisherEditorialCreatorPlugins, + ) + create: TrayPublisherCreatePluginsModel = Field( + title="Create", + default_factory=TrayPublisherCreatePluginsModel + ) + publish: TrayPublisherPublishPlugins = Field( + title="Publish Plugins", + default_factory=TrayPublisherPublishPlugins + ) + + +DEFAULT_TRAYPUBLISHER_SETTING = { + "simple_creators": DEFAULT_SIMPLE_CREATORS, + "editorial_creators": DEFAULT_EDITORIAL_CREATORS, + "create": DEFAULT_CREATORS, + "publish": DEFAULT_PUBLISH_PLUGINS, +} diff --git a/server_addon/traypublisher/server/settings/publish_plugins.py b/server_addon/traypublisher/server/settings/publish_plugins.py new file mode 100644 index 0000000000..3f00f3d52e --- /dev/null +++ b/server_addon/traypublisher/server/settings/publish_plugins.py @@ -0,0 +1,41 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class ValidatePluginModel(BaseSettingsModel): + _isGroup = True + enabled: bool = True + optional: bool = Field(True, title="Optional") + active: bool = Field(True, title="Active") + + +class ValidateFrameRangeModel(ValidatePluginModel): + """Allows to publish multiple video files in one go.
Name of matching + asset is parsed from file names ('asset.mov', 'asset_v001.mov', + 'my_asset_to_publish.mov')""" + + +class TrayPublisherPublishPlugins(BaseSettingsModel): + ValidateFrameRange: ValidateFrameRangeModel = Field( + title="Validate Frame Range", + default_factory=ValidateFrameRangeModel, + ) + ValidateExistingVersion: ValidatePluginModel = Field( + title="Validate Existing Version", + default_factory=ValidatePluginModel, + ) + + +DEFAULT_PUBLISH_PLUGINS = { + "ValidateFrameRange": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateExistingVersion": { + "enabled": True, + "optional": True, + "active": True + } +} diff --git a/server_addon/traypublisher/server/settings/simple_creators.py b/server_addon/traypublisher/server/settings/simple_creators.py new file mode 100644 index 0000000000..94d6602738 --- /dev/null +++ b/server_addon/traypublisher/server/settings/simple_creators.py @@ -0,0 +1,292 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class SimpleCreatorPlugin(BaseSettingsModel): + _layout = "expanded" + product_type: str = Field("", title="Product type") + # TODO add placeholder + identifier: str = Field("", title="Identifier") + label: str = Field("", title="Label") + icon: str = Field("", title="Icon") + default_variants: list[str] = Field( + default_factory=list, + title="Default Variants" + ) + description: str = Field( + "", + title="Description", + widget="textarea" + ) + detailed_description: str = Field( + "", + title="Detailed Description", + widget="textarea" + ) + allow_sequences: bool = Field( + False, + title="Allow sequences" + ) + allow_multiple_items: bool = Field( + False, + title="Allow multiple items" + ) + allow_version_control: bool = Field( + False, + title="Allow version control" + ) + extensions: list[str] = Field( + default_factory=list, + title="Extensions" + ) + + +DEFAULT_SIMPLE_CREATORS = [ + { + "product_type": "workfile", + "identifier": "", + "label": "Workfile", + "icon": "fa.file", + "default_variants": [ + "Main" + ], + "description": "Backup of a working scene", + "detailed_description": "Workfiles are full scenes from any application that are directly edited by artists. They represent a state of work on a task at a given point and are usually not directly referenced into other scenes.", + "allow_sequences": False, + "allow_multiple_items": False, + "allow_version_control": False, + "extensions": [ + ".ma", + ".mb", + ".nk", + ".hrox", + ".hip", + ".hiplc", + ".hipnc", + ".blend", + ".scn", + ".tvpp", + ".comp", + ".zip", + ".prproj", + ".drp", + ".psd", + ".psb", + ".aep" + ] + }, + { + "product_type": "model", + "identifier": "", + "label": "Model", + "icon": "fa.cubes", + "default_variants": [ + "Main", + "Proxy", + "Sculpt" + ], + "description": "Clean models", + "detailed_description": "Models should only contain geometry data, without any extras like cameras, locators or bones.\n\nKeep in mind that models published from tray publisher are not validated for correctness. ", + "allow_sequences": False, + "allow_multiple_items": True, + "allow_version_control": False, + "extensions": [ + ".ma", + ".mb", + ".obj", + ".abc", + ".fbx", + ".bgeo", + ".bgeogz", + ".bgeosc", + ".usd", + ".blend" + ] + }, + { + "product_type": "pointcache", + "identifier": "", + "label": "Pointcache", + "icon": "fa.gears", + "default_variants": [ + "Main" + ], + "description": "Geometry Caches", + "detailed_description": "Alembic or bgeo cache of animated data", + "allow_sequences": True, + "allow_multiple_items": True, + "allow_version_control": False, + "extensions": [ + ".abc", + ".bgeo", + ".bgeogz", + ".bgeosc" + ] + }, + { + "product_type": "plate", + "identifier": "", + "label": "Plate", + "icon": "mdi.camera-image", + "default_variants": [ + "Main", + "BG", + "Animatic", + "Reference", + "Offline" + ], + "description": "Footage Plates", + "detailed_description": "Any type of image seqeuence coming from outside of the studio. Usually camera footage, but could also be animatics used for reference.", + "allow_sequences": True, + "allow_multiple_items": True, + "allow_version_control": False, + "extensions": [ + ".exr", + ".png", + ".dpx", + ".jpg", + ".tiff", + ".tif", + ".mov", + ".mp4", + ".avi" + ] + }, + { + "product_type": "render", + "identifier": "", + "label": "Render", + "icon": "mdi.folder-multiple-image", + "default_variants": [], + "description": "Rendered images or video", + "detailed_description": "Sequence or single file renders", + "allow_sequences": True, + "allow_multiple_items": True, + "allow_version_control": False, + "extensions": [ + ".exr", + ".png", + ".dpx", + ".jpg", + ".jpeg", + ".tiff", + ".tif", + ".mov", + ".mp4", + ".avi" + ] + }, + { + "product_type": "camera", + "identifier": "", + "label": "Camera", + "icon": "fa.video-camera", + "default_variants": [], + "description": "3d Camera", + "detailed_description": "Ideally this should be only camera itself with baked animation, however, it can technically also include helper geometry.", + "allow_sequences": False, + "allow_multiple_items": True, + "allow_version_control": False, + "extensions": [ + ".abc", + ".ma", + ".hip", + ".blend", + ".fbx", + ".usd" + ] + }, + { + "product_type": "image", + "identifier": "", + "label": "Image", + "icon": "fa.image", + "default_variants": [ + "Reference", + "Texture", + "Concept", + "Background" + ], + "description": "Single image", + "detailed_description": "Any image data can be published as image product type. References, textures, concept art, matte paints. This is a fallback 2d product type for everything that doesn't fit more specific product type.", + "allow_sequences": False, + "allow_multiple_items": True, + "allow_version_control": False, + "extensions": [ + ".exr", + ".jpg", + ".jpeg", + ".dpx", + ".bmp", + ".tif", + ".tiff", + ".png", + ".psb", + ".psd" + ] + }, + { + "product_type": "vdb", + "identifier": "", + "label": "VDB Volumes", + "icon": "fa.cloud", + "default_variants": [], + "description": "Sparse volumetric data", + "detailed_description": "Hierarchical data structure for the efficient storage and manipulation of sparse volumetric data discretized on three-dimensional grids", + "allow_sequences": True, + "allow_multiple_items": True, + "allow_version_control": False, + "extensions": [ + ".vdb" + ] + }, + { + "product_type": "matchmove", + "identifier": "", + "label": "Matchmove", + "icon": "fa.empire", + "default_variants": [ + "Camera", + "Object", + "Mocap" + ], + "description": "Matchmoving script", + "detailed_description": "Script exported from matchmoving application to be later processed into a tracked camera with additional data", + "allow_sequences": False, + "allow_multiple_items": True, + "allow_version_control": False, + "extensions": [] + }, + { + "product_type": "rig", + "identifier": "", + "label": "Rig", + "icon": "fa.wheelchair", + "default_variants": [], + "description": "CG rig file", + "detailed_description": "CG rigged character or prop. Rig should be clean of any extra data and directly loadable into it's respective application\t", + "allow_sequences": False, + "allow_multiple_items": False, + "allow_version_control": False, + "extensions": [ + ".ma", + ".blend", + ".hip", + ".hda" + ] + }, + { + "product_type": "simpleUnrealTexture", + "identifier": "", + "label": "Simple UE texture", + "icon": "fa.image", + "default_variants": [], + "description": "Simple Unreal Engine texture", + "detailed_description": "Texture files with Unreal Engine naming conventions", + "allow_sequences": False, + "allow_multiple_items": True, + "allow_version_control": False, + "extensions": [] + } +] diff --git a/server_addon/traypublisher/server/version.py b/server_addon/traypublisher/server/version.py new file mode 100644 index 0000000000..a242f0e757 --- /dev/null +++ b/server_addon/traypublisher/server/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Package declaring addon version.""" +__version__ = "0.1.1" diff --git a/server_addon/tvpaint/server/__init__.py b/server_addon/tvpaint/server/__init__.py new file mode 100644 index 0000000000..033d7d3792 --- /dev/null +++ b/server_addon/tvpaint/server/__init__.py @@ -0,0 +1,17 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import TvpaintSettings, DEFAULT_VALUES + + +class TvpaintAddon(BaseServerAddon): + name = "tvpaint" + title = "TVPaint" + version = __version__ + settings_model: Type[TvpaintSettings] = TvpaintSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/tvpaint/server/settings/__init__.py b/server_addon/tvpaint/server/settings/__init__.py new file mode 100644 index 0000000000..abee32e897 --- /dev/null +++ b/server_addon/tvpaint/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + TvpaintSettings, + DEFAULT_VALUES, +) + + +__all__ = ( + "TvpaintSettings", + "DEFAULT_VALUES", +) diff --git a/server_addon/tvpaint/server/settings/create_plugins.py b/server_addon/tvpaint/server/settings/create_plugins.py new file mode 100644 index 0000000000..349bfdd288 --- /dev/null +++ b/server_addon/tvpaint/server/settings/create_plugins.py @@ -0,0 +1,133 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class CreateWorkfileModel(BaseSettingsModel): + enabled: bool = Field(True) + default_variant: str = Field(title="Default variant") + default_variants: list[str] = Field( + default_factory=list, title="Default variants") + + +class CreateReviewModel(BaseSettingsModel): + enabled: bool = Field(True) + active_on_create: bool = Field(True, title="Active by default") + default_variant: str = Field(title="Default variant") + default_variants: list[str] = Field( + default_factory=list, title="Default variants") + + +class CreateRenderSceneModel(BaseSettingsModel): + enabled: bool = Field(True) + active_on_create: bool = Field(True, title="Active by default") + mark_for_review: bool = Field(True, title="Review by default") + default_pass_name: str = Field(title="Default beauty pass") + default_variant: str = Field(title="Default variant") + default_variants: list[str] = Field( + default_factory=list, title="Default variants") + + +class CreateRenderLayerModel(BaseSettingsModel): + mark_for_review: bool = Field(True, title="Review by default") + default_pass_name: str = Field(title="Default beauty pass") + default_variant: str = Field(title="Default variant") + default_variants: list[str] = Field( + default_factory=list, title="Default variants") + + +class CreateRenderPassModel(BaseSettingsModel): + mark_for_review: bool = Field(True, title="Review by default") + default_variant: str = Field(title="Default variant") + default_variants: list[str] = Field( + default_factory=list, title="Default variants") + + +class AutoDetectCreateRenderModel(BaseSettingsModel): + """The creator tries to auto-detect Render Layers and Render Passes in scene. + + For Render Layers is used group name as a variant and for Render Passes is + used TVPaint layer name. + + Group names can be renamed by their used order in scene. The renaming + template where can be used '{group_index}' formatting key which is + filled by "used position index of group". + - Template: 'L{group_index}' + - Group offset: '10' + - Group padding: '3' + + Would create group names "L010", "L020", ... + """ + + enabled: bool = Field(True) + allow_group_rename: bool = Field(title="Allow group rename") + group_name_template: str = Field(title="Group name template") + group_idx_offset: int = Field(1, title="Group index Offset", ge=1) + group_idx_padding: int = Field(4, title="Group index Padding", ge=1) + + +class CreatePluginsModel(BaseSettingsModel): + create_workfile: CreateWorkfileModel = Field( + default_factory=CreateWorkfileModel, + title="Create Workfile" + ) + create_review: CreateReviewModel = Field( + default_factory=CreateReviewModel, + title="Create Review" + ) + create_render_scene: CreateRenderSceneModel = Field( + default_factory=CreateReviewModel, + title="Create Render Scene" + ) + create_render_layer: CreateRenderLayerModel= Field( + default_factory=CreateRenderLayerModel, + title="Create Render Layer" + ) + create_render_pass: CreateRenderPassModel = Field( + default_factory=CreateRenderPassModel, + title="Create Render Pass" + ) + auto_detect_render: AutoDetectCreateRenderModel = Field( + default_factory=AutoDetectCreateRenderModel, + title="Auto-Detect Create Render", + ) + + +DEFAULT_CREATE_SETTINGS = { + "create_workfile": { + "enabled": True, + "default_variant": "Main", + "default_variants": [] + }, + "create_review": { + "enabled": True, + "active_on_create": True, + "default_variant": "Main", + "default_variants": [] + }, + "create_render_scene": { + "enabled": True, + "active_on_create": False, + "mark_for_review": True, + "default_pass_name": "beauty", + "default_variant": "Main", + "default_variants": [] + }, + "create_render_layer": { + "mark_for_review": False, + "default_pass_name": "beauty", + "default_variant": "Main", + "default_variants": [] + }, + "create_render_pass": { + "mark_for_review": False, + "default_variant": "Main", + "default_variants": [] + }, + "auto_detect_render": { + "enabled": False, + "allow_group_rename": True, + "group_name_template": "L{group_index}", + "group_idx_offset": 10, + "group_idx_padding": 3 + } +} diff --git a/server_addon/tvpaint/server/settings/filters.py b/server_addon/tvpaint/server/settings/filters.py new file mode 100644 index 0000000000..009febae06 --- /dev/null +++ b/server_addon/tvpaint/server/settings/filters.py @@ -0,0 +1,19 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class FiltersSubmodel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: str = Field( + "", + title="Textarea", + widget="textarea", + ) + + +class PublishFiltersModel(BaseSettingsModel): + env_search_replace_values: list[FiltersSubmodel] = Field( + default_factory=list + ) diff --git a/server_addon/tvpaint/server/settings/imageio.py b/server_addon/tvpaint/server/settings/imageio.py new file mode 100644 index 0000000000..50f8b7eef4 --- /dev/null +++ b/server_addon/tvpaint/server/settings/imageio.py @@ -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 TVPaintImageIOModel(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" + ) diff --git a/server_addon/tvpaint/server/settings/main.py b/server_addon/tvpaint/server/settings/main.py new file mode 100644 index 0000000000..4cd6ac4b1a --- /dev/null +++ b/server_addon/tvpaint/server/settings/main.py @@ -0,0 +1,90 @@ +from pydantic import Field, validator +from ayon_server.settings import ( + BaseSettingsModel, + ensure_unique_names, +) + +from .imageio import TVPaintImageIOModel +from .workfile_builder import WorkfileBuilderPlugin +from .create_plugins import CreatePluginsModel, DEFAULT_CREATE_SETTINGS +from .publish_plugins import ( + PublishPluginsModel, + LoadPluginsModel, + DEFAULT_PUBLISH_SETTINGS, +) + + +class PublishGUIFilterItemModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: bool = Field(True, title="Active") + + +class PublishGUIFiltersModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: list[PublishGUIFilterItemModel] = Field(default_factory=list) + + @validator("value") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class TvpaintSettings(BaseSettingsModel): + imageio: TVPaintImageIOModel = Field( + default_factory=TVPaintImageIOModel, + title="Color Management (ImageIO)" + ) + stop_timer_on_application_exit: bool = Field( + title="Stop timer on application exit") + create: CreatePluginsModel = Field( + default_factory=CreatePluginsModel, + title="Create plugins" + ) + publish: PublishPluginsModel = Field( + default_factory=PublishPluginsModel, + title="Publish plugins") + load: LoadPluginsModel = Field( + default_factory=LoadPluginsModel, + title="Load plugins") + workfile_builder: WorkfileBuilderPlugin = Field( + default_factory=WorkfileBuilderPlugin, + title="Workfile Builder" + ) + filters: list[PublishGUIFiltersModel] = Field( + default_factory=list, + title="Publish GUI Filters") + + @validator("filters") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +DEFAULT_VALUES = { + "stop_timer_on_application_exit": False, + "create": DEFAULT_CREATE_SETTINGS, + "publish": DEFAULT_PUBLISH_SETTINGS, + "load": { + "LoadImage": { + "defaults": { + "stretch": True, + "timestretch": True, + "preload": True + } + }, + "ImportImage": { + "defaults": { + "stretch": True, + "timestretch": True, + "preload": True + } + } + }, + "workfile_builder": { + "create_first_version": False, + "custom_templates": [] + }, + "filters": [] +} diff --git a/server_addon/tvpaint/server/settings/publish_plugins.py b/server_addon/tvpaint/server/settings/publish_plugins.py new file mode 100644 index 0000000000..76c7eaac01 --- /dev/null +++ b/server_addon/tvpaint/server/settings/publish_plugins.py @@ -0,0 +1,132 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel +from ayon_server.types import ColorRGBA_uint8 + + +class CollectRenderInstancesModel(BaseSettingsModel): + ignore_render_pass_transparency: bool = Field( + title="Ignore Render Pass opacity" + ) + + +class ExtractSequenceModel(BaseSettingsModel): + """Review BG color is used for whole scene review and for thumbnails.""" + # TODO Use alpha color + review_bg: ColorRGBA_uint8 = Field( + (255, 255, 255, 1.0), + title="Review BG color") + + +class ValidatePluginModel(BaseSettingsModel): + enabled: bool = True + optional: bool = Field(True, title="Optional") + active: bool = Field(True, title="Active") + + +def compression_enum(): + return [ + {"value": "ZIP", "label": "ZIP"}, + {"value": "ZIPS", "label": "ZIPS"}, + {"value": "DWAA", "label": "DWAA"}, + {"value": "DWAB", "label": "DWAB"}, + {"value": "PIZ", "label": "PIZ"}, + {"value": "RLE", "label": "RLE"}, + {"value": "PXR24", "label": "PXR24"}, + {"value": "B44", "label": "B44"}, + {"value": "B44A", "label": "B44A"}, + {"value": "none", "label": "None"} + ] + + +class ExtractConvertToEXRModel(BaseSettingsModel): + """WARNING: This plugin does not work on MacOS (using OIIO tool).""" + enabled: bool = False + replace_pngs: bool = True + + exr_compression: str = Field( + "ZIP", + enum_resolver=compression_enum, + title="EXR Compression" + ) + + +class LoadImageDefaultModel(BaseSettingsModel): + _layout = "expanded" + stretch: bool = Field(title="Stretch") + timestretch: bool = Field(title="TimeStretch") + preload: bool = Field(title="Preload") + + +class LoadImageModel(BaseSettingsModel): + defaults: LoadImageDefaultModel = Field( + default_factory=LoadImageDefaultModel + ) + + +class PublishPluginsModel(BaseSettingsModel): + CollectRenderInstances: CollectRenderInstancesModel = Field( + default_factory=CollectRenderInstancesModel, + title="Collect Render Instances") + ExtractSequence: ExtractSequenceModel = Field( + default_factory=ExtractSequenceModel, + title="Extract Sequence") + ValidateProjectSettings: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Project Settings") + ValidateMarks: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate MarkIn/Out") + ValidateStartFrame: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Scene Start Frame") + ValidateAssetName: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Folder Name") + ExtractConvertToEXR: ExtractConvertToEXRModel = Field( + default_factory=ExtractConvertToEXRModel, + title="Extract Convert To EXR") + + +class LoadPluginsModel(BaseSettingsModel): + LoadImage: LoadImageModel = Field( + default_factory=LoadImageModel, + title="Load Image") + ImportImage: LoadImageModel = Field( + default_factory=LoadImageModel, + title="Import Image") + + +DEFAULT_PUBLISH_SETTINGS = { + "CollectRenderInstances": { + "ignore_render_pass_transparency": False + }, + "ExtractSequence": { + "review_bg": [255, 255, 255, 1.0] + }, + "ValidateProjectSettings": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMarks": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateStartFrame": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateAssetName": { + "enabled": True, + "optional": True, + "active": True + }, + "ExtractConvertToEXR": { + "enabled": False, + "replace_pngs": True, + "exr_compression": "ZIP" + } +} diff --git a/server_addon/tvpaint/server/settings/workfile_builder.py b/server_addon/tvpaint/server/settings/workfile_builder.py new file mode 100644 index 0000000000..e0aba5da7e --- /dev/null +++ b/server_addon/tvpaint/server/settings/workfile_builder.py @@ -0,0 +1,30 @@ +from pydantic import Field + +from ayon_server.settings import ( + BaseSettingsModel, + MultiplatformPathModel, + task_types_enum, +) + + +class CustomBuilderTemplate(BaseSettingsModel): + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + template_path: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel + ) + + +class WorkfileBuilderPlugin(BaseSettingsModel): + _title = "Workfile Builder" + create_first_version: bool = Field( + False, + title="Create first workfile" + ) + + custom_templates: list[CustomBuilderTemplate] = Field( + default_factory=CustomBuilderTemplate + ) diff --git a/server_addon/tvpaint/server/version.py b/server_addon/tvpaint/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/tvpaint/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/server_addon/unreal/server/__init__.py b/server_addon/unreal/server/__init__.py new file mode 100644 index 0000000000..a5f3e9597d --- /dev/null +++ b/server_addon/unreal/server/__init__.py @@ -0,0 +1,19 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import UnrealSettings, DEFAULT_VALUES + + +class UnrealAddon(BaseServerAddon): + name = "unreal" + title = "Unreal" + version = __version__ + settings_model: Type[UnrealSettings] = UnrealSettings + frontend_scopes = {} + services = {} + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/unreal/server/imageio.py b/server_addon/unreal/server/imageio.py new file mode 100644 index 0000000000..dde042ba47 --- /dev/null +++ b/server_addon/unreal/server/imageio.py @@ -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 UnrealImageIOModel(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" + ) diff --git a/server_addon/unreal/server/settings.py b/server_addon/unreal/server/settings.py new file mode 100644 index 0000000000..479e041e25 --- /dev/null +++ b/server_addon/unreal/server/settings.py @@ -0,0 +1,64 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + +from .imageio import UnrealImageIOModel + + +class ProjectSetup(BaseSettingsModel): + dev_mode: bool = Field( + False, + title="Dev mode" + ) + + +def _render_format_enum(): + return [ + {"value": "png", "label": "PNG"}, + {"value": "exr", "label": "EXR"}, + {"value": "jpg", "label": "JPG"}, + {"value": "bmp", "label": "BMP"} + ] + + +class UnrealSettings(BaseSettingsModel): + imageio: UnrealImageIOModel = Field( + default_factory=UnrealImageIOModel, + title="Color Management (ImageIO)" + ) + level_sequences_for_layouts: bool = Field( + False, + title="Generate level sequences when loading layouts" + ) + delete_unmatched_assets: bool = Field( + False, + title="Delete assets that are not matched" + ) + render_config_path: str = Field( + "", + title="Render Config Path" + ) + preroll_frames: int = Field( + 0, + title="Pre-roll frames" + ) + render_format: str = Field( + "png", + title="Render format", + enum_resolver=_render_format_enum + ) + project_setup: ProjectSetup = Field( + default_factory=ProjectSetup, + title="Project Setup", + ) + + +DEFAULT_VALUES = { + "level_sequences_for_layouts": False, + "delete_unmatched_assets": False, + "render_config_path": "", + "preroll_frames": 0, + "render_format": "png", + "project_setup": { + "dev_mode": False + } +} diff --git a/server_addon/unreal/server/version.py b/server_addon/unreal/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/unreal/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" From 7b5e716147a3fe977ae1431d69e64573c40255b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:20:36 +0200 Subject: [PATCH 376/446] Chore: Schemas inside OpenPype (#5354) * moved schemas to openpype content * fix schema discovery --- .../{schema.py => schema/__init__.py} | 10 +- openpype/pipeline/schema/application-1.0.json | 68 +++++++++ openpype/pipeline/schema/asset-1.0.json | 35 +++++ openpype/pipeline/schema/asset-2.0.json | 55 +++++++ openpype/pipeline/schema/asset-3.0.json | 55 +++++++ openpype/pipeline/schema/config-1.0.json | 85 +++++++++++ openpype/pipeline/schema/config-1.1.json | 87 +++++++++++ openpype/pipeline/schema/config-2.0.json | 87 +++++++++++ openpype/pipeline/schema/container-1.0.json | 100 ++++++++++++ openpype/pipeline/schema/container-2.0.json | 59 ++++++++ .../pipeline/schema/hero_version-1.0.json | 44 ++++++ openpype/pipeline/schema/inventory-1.0.json | 10 ++ openpype/pipeline/schema/inventory-1.1.json | 10 ++ openpype/pipeline/schema/project-2.0.json | 86 +++++++++++ openpype/pipeline/schema/project-2.1.json | 86 +++++++++++ openpype/pipeline/schema/project-3.0.json | 59 ++++++++ .../pipeline/schema/representation-1.0.json | 28 ++++ .../pipeline/schema/representation-2.0.json | 78 ++++++++++ openpype/pipeline/schema/session-1.0.json | 143 ++++++++++++++++++ openpype/pipeline/schema/session-2.0.json | 134 ++++++++++++++++ openpype/pipeline/schema/session-3.0.json | 81 ++++++++++ openpype/pipeline/schema/shaders-1.0.json | 32 ++++ openpype/pipeline/schema/subset-1.0.json | 35 +++++ openpype/pipeline/schema/subset-2.0.json | 51 +++++++ openpype/pipeline/schema/subset-3.0.json | 62 ++++++++ openpype/pipeline/schema/thumbnail-1.0.json | 42 +++++ openpype/pipeline/schema/version-1.0.json | 50 ++++++ openpype/pipeline/schema/version-2.0.json | 92 +++++++++++ openpype/pipeline/schema/version-3.0.json | 84 ++++++++++ openpype/pipeline/schema/workfile-1.0.json | 52 +++++++ 30 files changed, 1894 insertions(+), 6 deletions(-) rename openpype/pipeline/{schema.py => schema/__init__.py} (92%) create mode 100644 openpype/pipeline/schema/application-1.0.json create mode 100644 openpype/pipeline/schema/asset-1.0.json create mode 100644 openpype/pipeline/schema/asset-2.0.json create mode 100644 openpype/pipeline/schema/asset-3.0.json create mode 100644 openpype/pipeline/schema/config-1.0.json create mode 100644 openpype/pipeline/schema/config-1.1.json create mode 100644 openpype/pipeline/schema/config-2.0.json create mode 100644 openpype/pipeline/schema/container-1.0.json create mode 100644 openpype/pipeline/schema/container-2.0.json create mode 100644 openpype/pipeline/schema/hero_version-1.0.json create mode 100644 openpype/pipeline/schema/inventory-1.0.json create mode 100644 openpype/pipeline/schema/inventory-1.1.json create mode 100644 openpype/pipeline/schema/project-2.0.json create mode 100644 openpype/pipeline/schema/project-2.1.json create mode 100644 openpype/pipeline/schema/project-3.0.json create mode 100644 openpype/pipeline/schema/representation-1.0.json create mode 100644 openpype/pipeline/schema/representation-2.0.json create mode 100644 openpype/pipeline/schema/session-1.0.json create mode 100644 openpype/pipeline/schema/session-2.0.json create mode 100644 openpype/pipeline/schema/session-3.0.json create mode 100644 openpype/pipeline/schema/shaders-1.0.json create mode 100644 openpype/pipeline/schema/subset-1.0.json create mode 100644 openpype/pipeline/schema/subset-2.0.json create mode 100644 openpype/pipeline/schema/subset-3.0.json create mode 100644 openpype/pipeline/schema/thumbnail-1.0.json create mode 100644 openpype/pipeline/schema/version-1.0.json create mode 100644 openpype/pipeline/schema/version-2.0.json create mode 100644 openpype/pipeline/schema/version-3.0.json create mode 100644 openpype/pipeline/schema/workfile-1.0.json diff --git a/openpype/pipeline/schema.py b/openpype/pipeline/schema/__init__.py similarity index 92% rename from openpype/pipeline/schema.py rename to openpype/pipeline/schema/__init__.py index 7e96bfe1b1..d7b33f2621 100644 --- a/openpype/pipeline/schema.py +++ b/openpype/pipeline/schema/__init__.py @@ -24,6 +24,7 @@ log_ = logging.getLogger(__name__) ValidationError = jsonschema.ValidationError SchemaError = jsonschema.SchemaError +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) _CACHED = False @@ -121,17 +122,14 @@ def _precache(): """Store available schemas in-memory for reduced disk access""" global _CACHED - repos_root = os.environ["OPENPYPE_REPOS_ROOT"] - schema_dir = os.path.join(repos_root, "schema") - - for schema in os.listdir(schema_dir): + for schema in os.listdir(CURRENT_DIR): if schema.startswith(("_", ".")): continue if not schema.endswith(".json"): continue - if not os.path.isfile(os.path.join(schema_dir, schema)): + if not os.path.isfile(os.path.join(CURRENT_DIR, schema)): continue - with open(os.path.join(schema_dir, schema)) as f: + with open(os.path.join(CURRENT_DIR, schema)) as f: log_.debug("Installing schema '%s'.." % schema) _cache[schema] = json.load(f) _CACHED = True diff --git a/openpype/pipeline/schema/application-1.0.json b/openpype/pipeline/schema/application-1.0.json new file mode 100644 index 0000000000..953abee569 --- /dev/null +++ b/openpype/pipeline/schema/application-1.0.json @@ -0,0 +1,68 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:application-1.0", + "description": "An application definition.", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "label", + "application_dir", + "executable" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string" + }, + "label": { + "description": "Nice name of application.", + "type": "string" + }, + "application_dir": { + "description": "Name of directory used for application resources.", + "type": "string" + }, + "executable": { + "description": "Name of callable executable, this is called to launch the application", + "type": "string" + }, + "description": { + "description": "Description of application.", + "type": "string" + }, + "environment": { + "description": "Key/value pairs for environment variables related to this application. Supports lists for paths, such as PYTHONPATH.", + "type": "object", + "items": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + } + }, + "default_dirs": { + "type": "array", + "items": { + "type": "string" + } + }, + "copy": { + "type": "object", + "patternProperties": { + "^.*$": { + "anyOf": [ + {"type": "string"}, + {"type": "null"} + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/openpype/pipeline/schema/asset-1.0.json b/openpype/pipeline/schema/asset-1.0.json new file mode 100644 index 0000000000..ab104c002a --- /dev/null +++ b/openpype/pipeline/schema/asset-1.0.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:asset-1.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "name", + "subsets" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string" + }, + "name": { + "description": "Name of directory", + "type": "string" + }, + "subsets": { + "type": "array", + "items": { + "$ref": "subset.json" + } + } + }, + + "definitions": {} +} diff --git a/openpype/pipeline/schema/asset-2.0.json b/openpype/pipeline/schema/asset-2.0.json new file mode 100644 index 0000000000..b894d79792 --- /dev/null +++ b/openpype/pipeline/schema/asset-2.0.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:asset-2.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "silo", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["openpype:asset-2.0"], + "example": "openpype:asset-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["asset"], + "example": "asset" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of asset", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "Group or container of asset", + "type": "string", + "example": "assets" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} diff --git a/openpype/pipeline/schema/asset-3.0.json b/openpype/pipeline/schema/asset-3.0.json new file mode 100644 index 0000000000..948704d2a1 --- /dev/null +++ b/openpype/pipeline/schema/asset-3.0.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:asset-3.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["openpype:asset-3.0"], + "example": "openpype:asset-3.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["asset"], + "example": "asset" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of asset", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "Group or container of asset", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "assets" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} diff --git a/openpype/pipeline/schema/config-1.0.json b/openpype/pipeline/schema/config-1.0.json new file mode 100644 index 0000000000..49398a57cd --- /dev/null +++ b/openpype/pipeline/schema/config-1.0.json @@ -0,0 +1,85 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:config-1.0", + "description": "A project configuration.", + + "type": "object", + + "additionalProperties": false, + "required": [ + "tasks", + "apps" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string" + }, + "template": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^.*$": { + "type": "string" + } + } + }, + "tasks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "group": {"type": "string"}, + "label": {"type": "string"} + }, + "required": ["name"] + } + }, + "apps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "group": {"type": "string"}, + "label": {"type": "string"} + }, + "required": ["name"] + } + }, + "families": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "label": {"type": "string"}, + "hideFilter": {"type": "boolean"} + }, + "required": ["name"] + } + }, + "groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "color": {"type": "string"}, + "order": {"type": ["integer", "number"]} + }, + "required": ["name"] + } + }, + "copy": { + "type": "object" + } + } +} diff --git a/openpype/pipeline/schema/config-1.1.json b/openpype/pipeline/schema/config-1.1.json new file mode 100644 index 0000000000..6e15514aaf --- /dev/null +++ b/openpype/pipeline/schema/config-1.1.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:config-1.1", + "description": "A project configuration.", + + "type": "object", + + "additionalProperties": false, + "required": [ + "tasks", + "apps" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string" + }, + "template": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^.*$": { + "type": "string" + } + } + }, + "tasks": { + "type": "object", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "group": {"type": "string"}, + "label": {"type": "string"} + }, + "required": [ + "short_name" + ] + } + }, + "apps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "group": {"type": "string"}, + "label": {"type": "string"} + }, + "required": ["name"] + } + }, + "families": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "label": {"type": "string"}, + "hideFilter": {"type": "boolean"} + }, + "required": ["name"] + } + }, + "groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "color": {"type": "string"}, + "order": {"type": ["integer", "number"]} + }, + "required": ["name"] + } + }, + "copy": { + "type": "object" + } + } +} diff --git a/openpype/pipeline/schema/config-2.0.json b/openpype/pipeline/schema/config-2.0.json new file mode 100644 index 0000000000..54b226711a --- /dev/null +++ b/openpype/pipeline/schema/config-2.0.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:config-2.0", + "description": "A project configuration.", + + "type": "object", + + "additionalProperties": false, + "required": [ + "tasks", + "apps" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string" + }, + "templates": { + "type": "object" + }, + "roots": { + "type": "object" + }, + "imageio": { + "type": "object" + }, + "tasks": { + "type": "object", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "group": {"type": "string"}, + "label": {"type": "string"} + }, + "required": [ + "short_name" + ] + } + }, + "apps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "group": {"type": "string"}, + "label": {"type": "string"} + }, + "required": ["name"] + } + }, + "families": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "label": {"type": "string"}, + "hideFilter": {"type": "boolean"} + }, + "required": ["name"] + } + }, + "groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "color": {"type": "string"}, + "order": {"type": ["integer", "number"]} + }, + "required": ["name"] + } + }, + "copy": { + "type": "object" + } + } +} diff --git a/openpype/pipeline/schema/container-1.0.json b/openpype/pipeline/schema/container-1.0.json new file mode 100644 index 0000000000..012e8499e6 --- /dev/null +++ b/openpype/pipeline/schema/container-1.0.json @@ -0,0 +1,100 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:container-1.0", + "description": "A loaded asset", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "id", + "objectName", + "name", + "author", + "loader", + "families", + "time", + "subset", + "asset", + "representation", + "version", + "silo", + "path", + "source" + ], + "properties": { + "id": { + "description": "Identifier for finding object in host", + "type": "string", + "enum": ["pyblish.mindbender.container"], + "example": "pyblish.mindbender.container" + }, + "objectName": { + "description": "Name of internal object, such as the objectSet in Maya.", + "type": "string", + "example": "Bruce_:rigDefault_CON" + }, + "name": { + "description": "Full name of application object", + "type": "string", + "example": "modelDefault" + }, + "author": { + "description": "Name of the author of the published version", + "type": "string", + "example": "Marcus Ottosson" + }, + "loader": { + "description": "Name of loader plug-in used to produce this container", + "type": "string", + "example": "ModelLoader" + }, + "families": { + "description": "Families associated with the this subset", + "type": "string", + "example": "mindbender.model" + }, + "time": { + "description": "File-system safe, formatted time", + "type": "string", + "example": "20170329T131545Z" + }, + "subset": { + "description": "Name of source subset", + "type": "string", + "example": "modelDefault" + }, + "asset": { + "description": "Name of source asset", + "type": "string" , + "example": "Bruce" + }, + "representation": { + "description": "Name of source representation", + "type": "string" , + "example": ".ma" + }, + "version": { + "description": "Version number", + "type": "number", + "example": 12 + }, + "silo": { + "description": "Silo of parent asset", + "type": "string", + "example": "assets" + }, + "path": { + "description": "Absolute path on disk", + "type": "string", + "example": "{root}/assets/Bruce/publish/rigDefault/v002" + }, + "source": { + "description": "Absolute path to file from which this version was published", + "type": "string", + "example": "{root}/assets/Bruce/work/rigging/maya/scenes/rig_v001.ma" + } + } +} diff --git a/openpype/pipeline/schema/container-2.0.json b/openpype/pipeline/schema/container-2.0.json new file mode 100644 index 0000000000..1673ee5d1d --- /dev/null +++ b/openpype/pipeline/schema/container-2.0.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:container-2.0", + "description": "A loaded asset", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "id", + "objectName", + "name", + "namespace", + "loader", + "representation" + ], + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["openpype:container-2.0"], + "example": "openpype:container-2.0" + }, + "id": { + "description": "Identifier for finding object in host", + "type": "string", + "enum": ["pyblish.avalon.container"], + "example": "pyblish.avalon.container" + }, + "objectName": { + "description": "Name of internal object, such as the objectSet in Maya.", + "type": "string", + "example": "Bruce_:rigDefault_CON" + }, + "loader": { + "description": "Name of loader plug-in used to produce this container", + "type": "string", + "example": "ModelLoader" + }, + "name": { + "description": "Internal object name of container in application", + "type": "string", + "example": "modelDefault_01" + }, + "namespace": { + "description": "Internal namespace of container in application", + "type": "string", + "example": "Bruce_" + }, + "representation": { + "description": "Unique id of representation in database", + "type": "string", + "example": "59523f355f8c1b5f6c5e8348" + } + } +} diff --git a/openpype/pipeline/schema/hero_version-1.0.json b/openpype/pipeline/schema/hero_version-1.0.json new file mode 100644 index 0000000000..b720dc2887 --- /dev/null +++ b/openpype/pipeline/schema/hero_version-1.0.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:hero_version-1.0", + "description": "Hero version of asset", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "version_id", + "schema", + "type", + "parent" + ], + + "properties": { + "_id": { + "description": "Document's id (database will create it's if not entered)", + "example": "ObjectId(592c33475f8c1b064c4d1696)" + }, + "version_id": { + "description": "The version ID from which it was created", + "example": "ObjectId(592c33475f8c1b064c4d1695)" + }, + "schema": { + "description": "The schema associated with this document", + "type": "string", + "enum": ["openpype:hero_version-1.0"], + "example": "openpype:hero_version-1.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["hero_version"], + "example": "hero_version" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "ObjectId(592c33475f8c1b064c4d1697)" + } + } +} diff --git a/openpype/pipeline/schema/inventory-1.0.json b/openpype/pipeline/schema/inventory-1.0.json new file mode 100644 index 0000000000..2fe78794ab --- /dev/null +++ b/openpype/pipeline/schema/inventory-1.0.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:config-1.0", + "description": "A project configuration.", + + "type": "object", + + "additionalProperties": true +} diff --git a/openpype/pipeline/schema/inventory-1.1.json b/openpype/pipeline/schema/inventory-1.1.json new file mode 100644 index 0000000000..b61a76b32a --- /dev/null +++ b/openpype/pipeline/schema/inventory-1.1.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:config-1.1", + "description": "A project configuration.", + + "type": "object", + + "additionalProperties": true +} diff --git a/openpype/pipeline/schema/project-2.0.json b/openpype/pipeline/schema/project-2.0.json new file mode 100644 index 0000000000..0ed5a55599 --- /dev/null +++ b/openpype/pipeline/schema/project-2.0.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:project-2.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "data", + "config" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["openpype:project-2.0"], + "example": "openpype:project-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["project"], + "example": "project" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of directory", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "hulk" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": { + "fps": 24, + "width": 1920, + "height": 1080 + } + }, + "config": { + "type": "object", + "description": "Document metadata", + "example": { + "schema": "openpype:config-1.0", + "apps": [ + { + "name": "maya2016", + "label": "Autodesk Maya 2016" + }, + { + "name": "nuke10", + "label": "The Foundry Nuke 10.0" + } + ], + "tasks": [ + {"name": "model"}, + {"name": "render"}, + {"name": "animate"}, + {"name": "rig"}, + {"name": "lookdev"}, + {"name": "layout"} + ], + "template": { + "work": + "{root}/{project}/{silo}/{asset}/work/{task}/{app}", + "publish": + "{root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}" + } + }, + "$ref": "config-1.0.json" + } + }, + + "definitions": {} +} diff --git a/openpype/pipeline/schema/project-2.1.json b/openpype/pipeline/schema/project-2.1.json new file mode 100644 index 0000000000..9413c9f691 --- /dev/null +++ b/openpype/pipeline/schema/project-2.1.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:project-2.1", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "data", + "config" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["openpype:project-2.1"], + "example": "openpype:project-2.1" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["project"], + "example": "project" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of directory", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "hulk" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": { + "fps": 24, + "width": 1920, + "height": 1080 + } + }, + "config": { + "type": "object", + "description": "Document metadata", + "example": { + "schema": "openpype:config-1.1", + "apps": [ + { + "name": "maya2016", + "label": "Autodesk Maya 2016" + }, + { + "name": "nuke10", + "label": "The Foundry Nuke 10.0" + } + ], + "tasks": { + "Model": {"short_name": "mdl"}, + "Render": {"short_name": "rnd"}, + "Animate": {"short_name": "anim"}, + "Rig": {"short_name": "rig"}, + "Lookdev": {"short_name": "look"}, + "Layout": {"short_name": "lay"} + }, + "template": { + "work": + "{root}/{project}/{silo}/{asset}/work/{task}/{app}", + "publish": + "{root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}" + } + }, + "$ref": "config-1.1.json" + } + }, + + "definitions": {} +} diff --git a/openpype/pipeline/schema/project-3.0.json b/openpype/pipeline/schema/project-3.0.json new file mode 100644 index 0000000000..be23e10c93 --- /dev/null +++ b/openpype/pipeline/schema/project-3.0.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:project-3.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "data", + "config" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["openpype:project-3.0"], + "example": "openpype:project-3.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["project"], + "example": "project" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of directory", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "hulk" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": { + "fps": 24, + "width": 1920, + "height": 1080 + } + }, + "config": { + "type": "object", + "description": "Document metadata", + "$ref": "config-2.0.json" + } + }, + + "definitions": {} +} diff --git a/openpype/pipeline/schema/representation-1.0.json b/openpype/pipeline/schema/representation-1.0.json new file mode 100644 index 0000000000..347c585f52 --- /dev/null +++ b/openpype/pipeline/schema/representation-1.0.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:representation-1.0", + "description": "The inverse of an instance", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "format", + "path" + ], + + "properties": { + "schema": {"type": "string"}, + "format": { + "description": "File extension, including '.'", + "type": "string" + }, + "path": { + "description": "Unformatted path to version.", + "type": "string" + } + } +} diff --git a/openpype/pipeline/schema/representation-2.0.json b/openpype/pipeline/schema/representation-2.0.json new file mode 100644 index 0000000000..f47c16a10a --- /dev/null +++ b/openpype/pipeline/schema/representation-2.0.json @@ -0,0 +1,78 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:representation-2.0", + "description": "The inverse of an instance", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "parent", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["openpype:representation-2.0"], + "example": "openpype:representation-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["representation"], + "example": "representation" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of representation", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "abc" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": { + "label": "Alembic" + } + }, + "dependencies": { + "description": "Other representation that this representation depends on", + "type": "array", + "items": {"type": "string"}, + "example": [ + "592d547a5f8c1b388093c145" + ] + }, + "context": { + "description": "Summary of the context to which this representation belong.", + "type": "object", + "properties": { + "project": {"type": "object"}, + "asset": {"type": "string"}, + "silo": {"type": ["string", "null"]}, + "subset": {"type": "string"}, + "version": {"type": "number"}, + "representation": {"type": "string"} + }, + "example": { + "project": "hulk", + "asset": "Bruce", + "silo": "assets", + "subset": "rigDefault", + "version": 12, + "representation": "ma" + } + } + } +} diff --git a/openpype/pipeline/schema/session-1.0.json b/openpype/pipeline/schema/session-1.0.json new file mode 100644 index 0000000000..5ced0a6f08 --- /dev/null +++ b/openpype/pipeline/schema/session-1.0.json @@ -0,0 +1,143 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:session-1.0", + "description": "The Avalon environment", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "AVALON_PROJECTS", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_SILO", + "AVALON_CONFIG" + ], + + "properties": { + "AVALON_PROJECTS": { + "description": "Absolute path to root of project directories", + "type": "string", + "example": "/nas/projects" + }, + "AVALON_PROJECT": { + "description": "Name of project", + "type": "string", + "pattern": "^\\w*$", + "example": "Hulk" + }, + "AVALON_ASSET": { + "description": "Name of asset", + "type": "string", + "pattern": "^\\w*$", + "example": "Bruce" + }, + "AVALON_SILO": { + "description": "Name of asset group or container", + "type": "string", + "pattern": "^\\w*$", + "example": "assets" + }, + "AVALON_TASK": { + "description": "Name of task", + "type": "string", + "pattern": "^\\w*$", + "example": "modeling" + }, + "AVALON_CONFIG": { + "description": "Name of Avalon configuration", + "type": "string", + "pattern": "^\\w*$", + "example": "polly" + }, + "AVALON_APP": { + "description": "Name of application", + "type": "string", + "pattern": "^\\w*$", + "example": "maya2016" + }, + "AVALON_MONGO": { + "description": "Address to the asset database", + "type": "string", + "pattern": "^mongodb://[\\w/@:.]*$", + "example": "mongodb://localhost:27017", + "default": "mongodb://localhost:27017" + }, + "AVALON_DB": { + "description": "Name of database", + "type": "string", + "pattern": "^\\w*$", + "example": "avalon", + "default": "avalon" + }, + "AVALON_LABEL": { + "description": "Nice name of Avalon, used in e.g. graphical user interfaces", + "type": "string", + "example": "Mindbender", + "default": "Avalon" + }, + "AVALON_SENTRY": { + "description": "Address to Sentry", + "type": "string", + "pattern": "^http[\\w/@:.]*$", + "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2", + "default": null + }, + "AVALON_DEADLINE": { + "description": "Address to Deadline", + "type": "string", + "pattern": "^http[\\w/@:.]*$", + "example": "http://192.168.99.101", + "default": null + }, + "AVALON_TIMEOUT": { + "description": "Wherever there is a need for a timeout, this is the default value.", + "type": "string", + "pattern": "^[0-9]*$", + "default": "1000", + "example": "1000" + }, + "AVALON_UPLOAD": { + "description": "Boolean of whether to upload published material to central asset repository", + "type": "string", + "default": null, + "example": "True" + }, + "AVALON_USERNAME": { + "description": "Generic username", + "type": "string", + "pattern": "^\\w*$", + "default": "avalon", + "example": "myself" + }, + "AVALON_PASSWORD": { + "description": "Generic password", + "type": "string", + "pattern": "^\\w*$", + "default": "secret", + "example": "abc123" + }, + "AVALON_INSTANCE_ID": { + "description": "Unique identifier for instances in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.instance", + "example": "avalon.instance" + }, + "AVALON_CONTAINER_ID": { + "description": "Unique identifier for a loaded representation in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.container", + "example": "avalon.container" + }, + "AVALON_DEBUG": { + "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.", + "type": "string", + "default": null, + "example": "True" + } + } +} diff --git a/openpype/pipeline/schema/session-2.0.json b/openpype/pipeline/schema/session-2.0.json new file mode 100644 index 0000000000..0a4d51beb2 --- /dev/null +++ b/openpype/pipeline/schema/session-2.0.json @@ -0,0 +1,134 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:session-2.0", + "description": "The Avalon environment", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_CONFIG" + ], + + "properties": { + "AVALON_PROJECTS": { + "description": "Absolute path to root of project directories", + "type": "string", + "example": "/nas/projects" + }, + "AVALON_PROJECT": { + "description": "Name of project", + "type": "string", + "pattern": "^\\w*$", + "example": "Hulk" + }, + "AVALON_ASSET": { + "description": "Name of asset", + "type": "string", + "pattern": "^\\w*$", + "example": "Bruce" + }, + "AVALON_SILO": { + "description": "Name of asset group or container", + "type": "string", + "pattern": "^\\w*$", + "example": "assets" + }, + "AVALON_TASK": { + "description": "Name of task", + "type": "string", + "pattern": "^\\w*$", + "example": "modeling" + }, + "AVALON_CONFIG": { + "description": "Name of Avalon configuration", + "type": "string", + "pattern": "^\\w*$", + "example": "polly" + }, + "AVALON_APP": { + "description": "Name of application", + "type": "string", + "pattern": "^\\w*$", + "example": "maya2016" + }, + "AVALON_DB": { + "description": "Name of database", + "type": "string", + "pattern": "^\\w*$", + "example": "avalon", + "default": "avalon" + }, + "AVALON_LABEL": { + "description": "Nice name of Avalon, used in e.g. graphical user interfaces", + "type": "string", + "example": "Mindbender", + "default": "Avalon" + }, + "AVALON_SENTRY": { + "description": "Address to Sentry", + "type": "string", + "pattern": "^http[\\w/@:.]*$", + "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2", + "default": null + }, + "AVALON_DEADLINE": { + "description": "Address to Deadline", + "type": "string", + "pattern": "^http[\\w/@:.]*$", + "example": "http://192.168.99.101", + "default": null + }, + "AVALON_TIMEOUT": { + "description": "Wherever there is a need for a timeout, this is the default value.", + "type": "string", + "pattern": "^[0-9]*$", + "default": "1000", + "example": "1000" + }, + "AVALON_UPLOAD": { + "description": "Boolean of whether to upload published material to central asset repository", + "type": "string", + "default": null, + "example": "True" + }, + "AVALON_USERNAME": { + "description": "Generic username", + "type": "string", + "pattern": "^\\w*$", + "default": "avalon", + "example": "myself" + }, + "AVALON_PASSWORD": { + "description": "Generic password", + "type": "string", + "pattern": "^\\w*$", + "default": "secret", + "example": "abc123" + }, + "AVALON_INSTANCE_ID": { + "description": "Unique identifier for instances in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.instance", + "example": "avalon.instance" + }, + "AVALON_CONTAINER_ID": { + "description": "Unique identifier for a loaded representation in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.container", + "example": "avalon.container" + }, + "AVALON_DEBUG": { + "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.", + "type": "string", + "default": null, + "example": "True" + } + } +} diff --git a/openpype/pipeline/schema/session-3.0.json b/openpype/pipeline/schema/session-3.0.json new file mode 100644 index 0000000000..9f785939e4 --- /dev/null +++ b/openpype/pipeline/schema/session-3.0.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:session-3.0", + "description": "The Avalon environment", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "AVALON_PROJECT", + "AVALON_ASSET" + ], + + "properties": { + "AVALON_PROJECTS": { + "description": "Absolute path to root of project directories", + "type": "string", + "example": "/nas/projects" + }, + "AVALON_PROJECT": { + "description": "Name of project", + "type": "string", + "pattern": "^\\w*$", + "example": "Hulk" + }, + "AVALON_ASSET": { + "description": "Name of asset", + "type": "string", + "pattern": "^\\w*$", + "example": "Bruce" + }, + "AVALON_TASK": { + "description": "Name of task", + "type": "string", + "pattern": "^\\w*$", + "example": "modeling" + }, + "AVALON_APP": { + "description": "Name of host", + "type": "string", + "pattern": "^\\w*$", + "example": "maya2016" + }, + "AVALON_DB": { + "description": "Name of database", + "type": "string", + "pattern": "^\\w*$", + "example": "avalon", + "default": "avalon" + }, + "AVALON_LABEL": { + "description": "Nice name of Avalon, used in e.g. graphical user interfaces", + "type": "string", + "example": "Mindbender", + "default": "Avalon" + }, + "AVALON_TIMEOUT": { + "description": "Wherever there is a need for a timeout, this is the default value.", + "type": "string", + "pattern": "^[0-9]*$", + "default": "1000", + "example": "1000" + }, + "AVALON_INSTANCE_ID": { + "description": "Unique identifier for instances in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.instance", + "example": "avalon.instance" + }, + "AVALON_CONTAINER_ID": { + "description": "Unique identifier for a loaded representation in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.container", + "example": "avalon.container" + } + } +} diff --git a/openpype/pipeline/schema/shaders-1.0.json b/openpype/pipeline/schema/shaders-1.0.json new file mode 100644 index 0000000000..7102ba1861 --- /dev/null +++ b/openpype/pipeline/schema/shaders-1.0.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:shaders-1.0", + "description": "Relationships between shaders and Avalon IDs", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "shader" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string" + }, + "shader": { + "description": "Name of directory", + "type": "array", + "items": { + "type": "str", + "description": "Avalon ID and optional face indexes, e.g. 'f9520572-ac1d-11e6-b39e-3085a99791c9.f[5002:5185]'" + } + } + }, + + "definitions": {} +} diff --git a/openpype/pipeline/schema/subset-1.0.json b/openpype/pipeline/schema/subset-1.0.json new file mode 100644 index 0000000000..a299a6d341 --- /dev/null +++ b/openpype/pipeline/schema/subset-1.0.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:subset-1.0", + "description": "A container of instances", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "name", + "versions" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string" + }, + "name": { + "description": "Name of directory", + "type": "string" + }, + "versions": { + "type": "array", + "items": { + "$ref": "version.json" + } + } + }, + + "definitions": {} +} diff --git a/openpype/pipeline/schema/subset-2.0.json b/openpype/pipeline/schema/subset-2.0.json new file mode 100644 index 0000000000..db256ec7fb --- /dev/null +++ b/openpype/pipeline/schema/subset-2.0.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:subset-2.0", + "description": "A container of instances", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "parent", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "The schema associated with this document", + "type": "string", + "enum": ["openpype:subset-2.0"], + "example": "openpype:subset-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["subset"], + "example": "subset" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of directory", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "shot01" + }, + "data": { + "type": "object", + "description": "Document metadata", + "example": { + "frameStart": 1000, + "frameEnd": 1201 + } + } + } +} diff --git a/openpype/pipeline/schema/subset-3.0.json b/openpype/pipeline/schema/subset-3.0.json new file mode 100644 index 0000000000..1a0db53c04 --- /dev/null +++ b/openpype/pipeline/schema/subset-3.0.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:subset-3.0", + "description": "A container of instances", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "parent", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "The schema associated with this document", + "type": "string", + "enum": ["openpype:subset-3.0"], + "example": "openpype:subset-3.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["subset"], + "example": "subset" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of directory", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "shot01" + }, + "data": { + "description": "Document metadata", + "type": "object", + "required": ["families"], + "properties": { + "families": { + "type": "array", + "items": {"type": "string"}, + "description": "One or more families associated with this subset" + } + }, + "example": { + "families" : [ + "avalon.camera" + ], + "frameStart": 1000, + "frameEnd": 1201 + } + } + } +} diff --git a/openpype/pipeline/schema/thumbnail-1.0.json b/openpype/pipeline/schema/thumbnail-1.0.json new file mode 100644 index 0000000000..5bdf78a4b1 --- /dev/null +++ b/openpype/pipeline/schema/thumbnail-1.0.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:thumbnail-1.0", + "description": "Entity with thumbnail data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "data" + ], + + "properties": { + "schema": { + "description": "The schema associated with this document", + "type": "string", + "enum": ["openpype:thumbnail-1.0"], + "example": "openpype:thumbnail-1.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["thumbnail"], + "example": "thumbnail" + }, + "data": { + "description": "Thumbnail data", + "type": "object", + "example": { + "binary_data": "Binary({byte data of image})", + "template": "{thumbnail_root}/{project[name]}/{_id}{ext}}", + "template_data": { + "ext": ".jpg" + } + } + } + } +} diff --git a/openpype/pipeline/schema/version-1.0.json b/openpype/pipeline/schema/version-1.0.json new file mode 100644 index 0000000000..daa1997721 --- /dev/null +++ b/openpype/pipeline/schema/version-1.0.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:version-1.0", + "description": "An individual version", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "version", + "path", + "time", + "author", + "source", + "representations" + ], + + "properties": { + "schema": {"type": "string"}, + "representations": { + "type": "array", + "items": { + "$ref": "representation.json" + } + }, + "time": { + "description": "ISO formatted, file-system compatible time", + "type": "string" + }, + "author": { + "description": "User logged on to the machine at time of publish", + "type": "string" + }, + "version": { + "description": "Number of this version", + "type": "number" + }, + "path": { + "description": "Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001", + "type": "string" + }, + "source": { + "description": "Original file from which this version was made.", + "type": "string" + } + } +} diff --git a/openpype/pipeline/schema/version-2.0.json b/openpype/pipeline/schema/version-2.0.json new file mode 100644 index 0000000000..099e9be70a --- /dev/null +++ b/openpype/pipeline/schema/version-2.0.json @@ -0,0 +1,92 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:version-2.0", + "description": "An individual version", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "parent", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "The schema associated with this document", + "type": "string", + "enum": ["openpype:version-2.0"], + "example": "openpype:version-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["version"], + "example": "version" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Number of version", + "type": "number", + "example": 12 + }, + "locations": { + "description": "Where on the planet this version can be found.", + "type": "array", + "items": {"type": "string"}, + "example": ["data.avalon.com"] + }, + "data": { + "description": "Document metadata", + "type": "object", + "required": ["families", "author", "source", "time"], + "properties": { + "time": { + "description": "ISO formatted, file-system compatible time", + "type": "string" + }, + "timeFormat": { + "description": "ISO format of time", + "type": "string" + }, + "author": { + "description": "User logged on to the machine at time of publish", + "type": "string" + }, + "version": { + "description": "Number of this version", + "type": "number" + }, + "path": { + "description": "Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001", + "type": "string" + }, + "source": { + "description": "Original file from which this version was made.", + "type": "string" + }, + "families": { + "type": "array", + "items": {"type": "string"}, + "description": "One or more families associated with this version" + } + }, + "example": { + "source" : "{root}/f02_prod/assets/BubbleWitch/work/modeling/marcus/maya/scenes/model_v001.ma", + "author" : "marcus", + "families" : [ + "avalon.model" + ], + "time" : "20170510T090203Z" + } + } + } +} diff --git a/openpype/pipeline/schema/version-3.0.json b/openpype/pipeline/schema/version-3.0.json new file mode 100644 index 0000000000..3e07fc4499 --- /dev/null +++ b/openpype/pipeline/schema/version-3.0.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:version-3.0", + "description": "An individual version", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "parent", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "The schema associated with this document", + "type": "string", + "enum": ["openpype:version-3.0"], + "example": "openpype:version-3.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["version"], + "example": "version" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Number of version", + "type": "number", + "example": 12 + }, + "locations": { + "description": "Where on the planet this version can be found.", + "type": "array", + "items": {"type": "string"}, + "example": ["data.avalon.com"] + }, + "data": { + "description": "Document metadata", + "type": "object", + "required": ["author", "source", "time"], + "properties": { + "time": { + "description": "ISO formatted, file-system compatible time", + "type": "string" + }, + "timeFormat": { + "description": "ISO format of time", + "type": "string" + }, + "author": { + "description": "User logged on to the machine at time of publish", + "type": "string" + }, + "version": { + "description": "Number of this version", + "type": "number" + }, + "path": { + "description": "Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001", + "type": "string" + }, + "source": { + "description": "Original file from which this version was made.", + "type": "string" + } + }, + "example": { + "source" : "{root}/f02_prod/assets/BubbleWitch/work/modeling/marcus/maya/scenes/model_v001.ma", + "author" : "marcus", + "time" : "20170510T090203Z" + } + } + } +} diff --git a/openpype/pipeline/schema/workfile-1.0.json b/openpype/pipeline/schema/workfile-1.0.json new file mode 100644 index 0000000000..5f9600ef20 --- /dev/null +++ b/openpype/pipeline/schema/workfile-1.0.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:workfile-1.0", + "description": "Workfile additional information.", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "filename", + "task_name", + "parent" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["openpype:workfile-1.0"], + "example": "openpype:workfile-1.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["workfile"], + "example": "workfile" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "filename": { + "description": "Workfile's filename", + "type": "string", + "example": "kuba_each_case_Alpaca_01_animation_v001.ma" + }, + "task_name": { + "description": "Task name", + "type": "string", + "example": "animation" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + } +} From 2bc8b49b9c5005950b481904a7ee3efdc0bd99bf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 27 Jul 2023 09:52:17 +0100 Subject: [PATCH 377/446] Use more appropriate name for function Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/unreal/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 5b2e35958b..0c39773c19 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -445,7 +445,7 @@ def check_built_plugin_existance(plugin_path) -> bool: return True -def move_built_plugin(engine_path: Path, plugin_path: Path) -> None: +def copy_built_plugin(engine_path: Path, plugin_path: Path) -> None: ayon_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" if not ayon_plugin_path.is_dir(): From d63aa34a767249d5a3c06d32efc83d23bacbc622 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:54:45 +0200 Subject: [PATCH 378/446] AYON: 3rd party addon usage (#5300) * implemented helper functions to get ffmpeg and oiio tool arguments * modified validation functions to be able to handle list of arguments * path getters can return a path in AYON mode if one argument is returned * removed test exception * modified docstrings * is_oiio_supported is using new functions to get launch arguments * new functions are in lib public = * use new functions all over the place * renamed 'ffmpeg_path' to 'ffmpeg_args' * raise 'ToolNotFoundError' if tool argument could not be found * reraise 'KnownPublishError' in publish plugins * fix comment * simplify args start * ffmpeg and oiio function require tool name and support additional arguments * renamed 'get_oiio_tools_args' to 'get_oiio_tool_args' * fix variable name --- .../harmony/plugins/publish/extract_render.py | 7 +- .../hiero/plugins/publish/extract_frames.py | 6 +- .../maya/plugins/publish/extract_look.py | 24 +-- .../plugins/publish/extract_review.py | 29 ++-- .../plugins/publish/extract_thumbnail.py | 7 +- .../plugins/publish/extract_convert_to_exr.py | 18 +- openpype/lib/__init__.py | 10 +- openpype/lib/transcoding.py | 34 ++-- openpype/lib/vendor_bin_utils.py | 164 +++++++++++++++--- .../publish/extract_otio_audio_tracks.py | 19 +- .../plugins/publish/extract_otio_review.py | 6 +- .../publish/extract_otio_trimming_video.py | 6 +- openpype/plugins/publish/extract_review.py | 11 +- .../plugins/publish/extract_review_slate.py | 21 +-- .../plugins/publish/extract_scanline_exr.py | 20 ++- openpype/plugins/publish/extract_thumbnail.py | 34 ++-- .../publish/extract_thumbnail_from_source.py | 19 +- .../publish/extract_trim_video_audio.py | 7 +- openpype/scripts/otio_burnin.py | 18 +- .../publisher/widgets/thumbnail_widget.py | 16 +- .../widgets/widget_drop_frame.py | 24 +-- 21 files changed, 302 insertions(+), 198 deletions(-) diff --git a/openpype/hosts/harmony/plugins/publish/extract_render.py b/openpype/hosts/harmony/plugins/publish/extract_render.py index 38b09902c1..5825d95a4a 100644 --- a/openpype/hosts/harmony/plugins/publish/extract_render.py +++ b/openpype/hosts/harmony/plugins/publish/extract_render.py @@ -94,15 +94,14 @@ class ExtractRender(pyblish.api.InstancePlugin): # Generate thumbnail. thumbnail_path = os.path.join(path, "thumbnail.png") - ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") - args = [ - ffmpeg_path, + args = openpype.lib.get_ffmpeg_tool_args( + "ffmpeg", "-y", "-i", os.path.join(path, list(collections[0])[0]), "-vf", "scale=300:-1", "-vframes", "1", thumbnail_path - ] + ) process = subprocess.Popen( args, stdout=subprocess.PIPE, diff --git a/openpype/hosts/hiero/plugins/publish/extract_frames.py b/openpype/hosts/hiero/plugins/publish/extract_frames.py index f865d2fb39..803c338766 100644 --- a/openpype/hosts/hiero/plugins/publish/extract_frames.py +++ b/openpype/hosts/hiero/plugins/publish/extract_frames.py @@ -2,7 +2,7 @@ import os import pyblish.api from openpype.lib import ( - get_oiio_tools_path, + get_oiio_tool_args, run_subprocess, ) from openpype.pipeline import publish @@ -18,7 +18,7 @@ class ExtractFrames(publish.Extractor): movie_extensions = ["mov", "mp4"] def process(self, instance): - oiio_tool_path = get_oiio_tools_path() + oiio_tool_args = get_oiio_tool_args("oiiotool") staging_dir = self.staging_dir(instance) output_template = os.path.join(staging_dir, instance.data["name"]) sequence = instance.context.data["activeTimeline"] @@ -36,7 +36,7 @@ class ExtractFrames(publish.Extractor): output_path = output_template output_path += ".{:04d}.{}".format(int(frame), output_ext) - args = [oiio_tool_path] + args = list(oiio_tool_args) ext = os.path.splitext(input_path)[1][1:] if ext in self.movie_extensions: diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index e2c88ef44a..b13568c781 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -15,8 +15,14 @@ import pyblish.api from maya import cmds # noqa -from openpype.lib.vendor_bin_utils import find_executable -from openpype.lib import source_hash, run_subprocess, get_oiio_tools_path +from openpype.lib import ( + find_executable, + source_hash, + run_subprocess, + get_oiio_tool_args, + ToolNotFoundError, +) + from openpype.pipeline import legacy_io, publish, KnownPublishError from openpype.hosts.maya.api import lib @@ -267,12 +273,11 @@ class MakeTX(TextureProcessor): """ - maketx_path = get_oiio_tools_path("maketx") - - if not maketx_path: - raise AssertionError( - "OIIO 'maketx' tool not found. Result: {}".format(maketx_path) - ) + try: + maketx_args = get_oiio_tool_args("maketx") + except ToolNotFoundError: + raise KnownPublishError( + "OpenImageIO is not available on the machine") # Define .tx filepath in staging if source file is not .tx fname, ext = os.path.splitext(os.path.basename(source)) @@ -328,8 +333,7 @@ class MakeTX(TextureProcessor): self.log.info("Generating .tx file for %s .." % source) - subprocess_args = [ - maketx_path, + subprocess_args = maketx_args + [ "-v", # verbose "-u", # update mode # --checknan doesn't influence the output file but aborts the diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index d5416a389d..4aa7a05bd1 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -1,10 +1,9 @@ import os -import shutil from PIL import Image from openpype.lib import ( run_subprocess, - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, ) from openpype.pipeline import publish from openpype.hosts.photoshop import api as photoshop @@ -85,7 +84,7 @@ class ExtractReview(publish.Extractor): instance.data["representations"].append(repre_skeleton) processed_img_names = [img_list] - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") + ffmpeg_args = get_ffmpeg_tool_args("ffmpeg") instance.data["stagingDir"] = staging_dir @@ -94,13 +93,21 @@ class ExtractReview(publish.Extractor): source_files_pattern = self._check_and_resize(processed_img_names, source_files_pattern, staging_dir) - self._generate_thumbnail(ffmpeg_path, instance, source_files_pattern, - staging_dir) + self._generate_thumbnail( + list(ffmpeg_args), + instance, + source_files_pattern, + staging_dir) no_of_frames = len(processed_img_names) if no_of_frames > 1: - self._generate_mov(ffmpeg_path, instance, fps, no_of_frames, - source_files_pattern, staging_dir) + self._generate_mov( + list(ffmpeg_args), + instance, + fps, + no_of_frames, + source_files_pattern, + staging_dir) self.log.info(f"Extracted {instance} to {staging_dir}") @@ -142,8 +149,9 @@ class ExtractReview(publish.Extractor): "tags": self.mov_options['tags'] }) - def _generate_thumbnail(self, ffmpeg_path, instance, source_files_pattern, - staging_dir): + def _generate_thumbnail( + self, ffmpeg_args, instance, source_files_pattern, staging_dir + ): """Generates scaled down thumbnail and adds it as representation. Args: @@ -157,8 +165,7 @@ class ExtractReview(publish.Extractor): # Generate thumbnail thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") self.log.info(f"Generate thumbnail {thumbnail_path}") - args = [ - ffmpeg_path, + args = ffmpeg_args + [ "-y", "-i", source_files_pattern, "-vf", "scale=300:-1", diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 9f02d65d00..b99503b3c8 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -1,8 +1,9 @@ import os +import subprocess import tempfile import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, get_ffprobe_streams, path_to_subprocess_arg, run_subprocess, @@ -62,12 +63,12 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): instance.context.data["cleanupFullPaths"].append(full_thumbnail_path) - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") + ffmpeg_executable_args = get_ffmpeg_tool_args("ffmpeg") ffmpeg_args = self.ffmpeg_args or {} jpeg_items = [ - path_to_subprocess_arg(ffmpeg_path), + subprocess.list2cmdline(ffmpeg_executable_args), # override file if already exists "-y" ] diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py index ab5bbc5e2c..c10fc4de97 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py @@ -9,7 +9,8 @@ import json import pyblish.api from openpype.lib import ( - get_oiio_tools_path, + get_oiio_tool_args, + ToolNotFoundError, run_subprocess, ) from openpype.pipeline import KnownPublishError @@ -34,11 +35,12 @@ class ExtractConvertToEXR(pyblish.api.InstancePlugin): if not repres: return - oiio_path = get_oiio_tools_path() - # Raise an exception when oiiotool is not available - # - this can currently happen on MacOS machines - if not os.path.exists(oiio_path): - KnownPublishError( + try: + oiio_args = get_oiio_tool_args("oiiotool") + except ToolNotFoundError: + # Raise an exception when oiiotool is not available + # - this can currently happen on MacOS machines + raise KnownPublishError( "OpenImageIO tool is not available on this machine." ) @@ -64,8 +66,8 @@ class ExtractConvertToEXR(pyblish.api.InstancePlugin): src_filepaths.add(src_filepath) - args = [ - oiio_path, src_filepath, + args = oiio_args + [ + src_filepath, "--compression", self.exr_compression, # TODO how to define color conversion? "--colorconvert", "sRGB", "linear", diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 9065588cf1..40df264452 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -22,11 +22,14 @@ from .events import ( ) from .vendor_bin_utils import ( + ToolNotFoundError, find_executable, get_vendor_bin_path, get_oiio_tools_path, + get_oiio_tool_args, get_ffmpeg_tool_path, - is_oiio_supported + get_ffmpeg_tool_args, + is_oiio_supported, ) from .attribute_definitions import ( @@ -172,7 +175,6 @@ __all__ = [ "emit_event", "register_event_callback", - "find_executable", "get_openpype_execute_args", "get_linux_launcher_args", "execute", @@ -186,9 +188,13 @@ __all__ = [ "env_value_to_bool", "get_paths_from_environ", + "ToolNotFoundError", + "find_executable", "get_vendor_bin_path", "get_oiio_tools_path", + "get_oiio_tool_args", "get_ffmpeg_tool_path", + "get_ffmpeg_tool_args", "is_oiio_supported", "AbstractAttrDef", diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index de6495900e..2bae28786e 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -11,8 +11,8 @@ import xml.etree.ElementTree from .execute import run_subprocess from .vendor_bin_utils import ( - get_ffmpeg_tool_path, - get_oiio_tools_path, + get_ffmpeg_tool_args, + get_oiio_tool_args, is_oiio_supported, ) @@ -83,11 +83,11 @@ def get_oiio_info_for_input(filepath, logger=None, subimages=False): Stdout should contain xml format string. """ - args = [ - get_oiio_tools_path(), + args = get_oiio_tool_args( + "oiiotool", "--info", "-v" - ] + ) if subimages: args.append("-a") @@ -486,12 +486,11 @@ def convert_for_ffmpeg( compression = "none" # Prepare subprocess arguments - oiio_cmd = [ - get_oiio_tools_path(), - + oiio_cmd = get_oiio_tool_args( + "oiiotool", # Don't add any additional attributes "--nosoftwareattrib", - ] + ) # Add input compression if available if compression: oiio_cmd.extend(["--compression", compression]) @@ -656,12 +655,11 @@ def convert_input_paths_for_ffmpeg( for input_path in input_paths: # Prepare subprocess arguments - oiio_cmd = [ - get_oiio_tools_path(), - + oiio_cmd = get_oiio_tool_args( + "oiiotool", # Don't add any additional attributes "--nosoftwareattrib", - ] + ) # Add input compression if available if compression: oiio_cmd.extend(["--compression", compression]) @@ -729,8 +727,8 @@ def get_ffprobe_data(path_to_file, logger=None): logger.info( "Getting information about input \"{}\".".format(path_to_file) ) - args = [ - get_ffmpeg_tool_path("ffprobe"), + ffprobe_args = get_ffmpeg_tool_args("ffprobe") + args = ffprobe_args + [ "-hide_banner", "-loglevel", "fatal", "-show_error", @@ -1084,13 +1082,13 @@ def convert_colorspace( if logger is None: logger = logging.getLogger(__name__) - oiio_cmd = [ - get_oiio_tools_path(), + oiio_cmd = get_oiio_tool_args( + "oiiotool", input_path, # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path - ] + ) if all([target_colorspace, view, display]): raise ValueError("Colorspace and both screen and display" diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index f27c78d486..dc8bb7435e 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -3,9 +3,15 @@ import logging import platform import subprocess +from openpype import AYON_SERVER_ENABLED + log = logging.getLogger("Vendor utils") +class ToolNotFoundError(Exception): + """Raised when tool arguments are not found.""" + + class CachedToolPaths: """Cache already used and discovered tools and their executables. @@ -252,7 +258,7 @@ def _check_args_returncode(args): return proc.returncode == 0 -def _oiio_executable_validation(filepath): +def _oiio_executable_validation(args): """Validate oiio tool executable if can be executed. Validation has 2 steps. First is using 'find_executable' to fill possible @@ -270,32 +276,63 @@ def _oiio_executable_validation(filepath): should be used. Args: - filepath (str): Path to executable. + args (Union[str, list[str]]): Arguments to launch tool or + path to tool executable. Returns: bool: Filepath is valid executable. """ - filepath = find_executable(filepath) - if not filepath: + if not args: return False - return _check_args_returncode([filepath, "--help"]) + if not isinstance(args, list): + filepath = find_executable(args) + if not filepath: + return False + args = [filepath] + return _check_args_returncode(args + ["--help"]) + + +def _get_ayon_oiio_tool_args(tool_name): + try: + # Use 'ayon-third-party' addon to get oiio arguments + from ayon_third_party import get_oiio_arguments + except Exception: + print("!!! Failed to import 'ayon_third_party' addon.") + return None + + try: + return get_oiio_arguments(tool_name) + except Exception as exc: + print("!!! Failed to get OpenImageIO args. Reason: {}".format(exc)) + return None def get_oiio_tools_path(tool="oiiotool"): - """Path to vendorized OpenImageIO tool executables. + """Path to OpenImageIO tool executables. - On Window it adds .exe extension if missing from tool argument. + On Windows it adds .exe extension if missing from tool argument. Args: - tool (string): Tool name (oiiotool, maketx, ...). + tool (string): Tool name 'oiiotool', 'maketx', etc. Default is "oiiotool". """ if CachedToolPaths.is_tool_cached(tool): return CachedToolPaths.get_executable_path(tool) + if AYON_SERVER_ENABLED: + args = _get_ayon_oiio_tool_args(tool) + if args: + if len(args) > 1: + raise ValueError( + "AYON oiio arguments consist of multiple arguments." + ) + tool_executable_path = args[0] + CachedToolPaths.cache_executable_path(tool, tool_executable_path) + return tool_executable_path + custom_paths_str = os.environ.get("OPENPYPE_OIIO_PATHS") or "" tool_executable_path = find_tool_in_custom_paths( custom_paths_str.split(os.pathsep), @@ -321,7 +358,33 @@ def get_oiio_tools_path(tool="oiiotool"): return tool_executable_path -def _ffmpeg_executable_validation(filepath): +def get_oiio_tool_args(tool_name, *extra_args): + """Arguments to launch OpenImageIO tool. + + Args: + tool_name (str): Tool name 'oiiotool', 'maketx', etc. + *extra_args (str): Extra arguments to add to after tool arguments. + + Returns: + list[str]: List of arguments. + """ + + extra_args = list(extra_args) + + if AYON_SERVER_ENABLED: + args = _get_ayon_oiio_tool_args(tool_name) + if args: + return args + extra_args + + path = get_oiio_tools_path(tool_name) + if path: + return [path] + extra_args + raise ToolNotFoundError( + "OIIO '{}' tool not found.".format(tool_name) + ) + + +def _ffmpeg_executable_validation(args): """Validate ffmpeg tool executable if can be executed. Validation has 2 steps. First is using 'find_executable' to fill possible @@ -338,24 +401,45 @@ def _ffmpeg_executable_validation(filepath): It does not validate if the executable is really a ffmpeg tool. Args: - filepath (str): Path to executable. + args (Union[str, list[str]]): Arguments to launch tool or + path to tool executable. Returns: bool: Filepath is valid executable. """ - filepath = find_executable(filepath) - if not filepath: + if not args: return False - return _check_args_returncode([filepath, "-version"]) + if not isinstance(args, list): + filepath = find_executable(args) + if not filepath: + return False + args = [filepath] + return _check_args_returncode(args + ["--help"]) + + +def _get_ayon_ffmpeg_tool_args(tool_name): + try: + # Use 'ayon-third-party' addon to get ffmpeg arguments + from ayon_third_party import get_ffmpeg_arguments + + except Exception: + print("!!! Failed to import 'ayon_third_party' addon.") + return None + + try: + return get_ffmpeg_arguments(tool_name) + except Exception as exc: + print("!!! Failed to get FFmpeg args. Reason: {}".format(exc)) + return None def get_ffmpeg_tool_path(tool="ffmpeg"): """Path to vendorized FFmpeg executable. Args: - tool (string): Tool name (ffmpeg, ffprobe, ...). + tool (str): Tool name 'ffmpeg', 'ffprobe', etc. Default is "ffmpeg". Returns: @@ -365,6 +449,17 @@ def get_ffmpeg_tool_path(tool="ffmpeg"): if CachedToolPaths.is_tool_cached(tool): return CachedToolPaths.get_executable_path(tool) + if AYON_SERVER_ENABLED: + args = _get_ayon_ffmpeg_tool_args(tool) + if args is not None: + if len(args) > 1: + raise ValueError( + "AYON ffmpeg arguments consist of multiple arguments." + ) + tool_executable_path = args[0] + CachedToolPaths.cache_executable_path(tool, tool_executable_path) + return tool_executable_path + custom_paths_str = os.environ.get("OPENPYPE_FFMPEG_PATHS") or "" tool_executable_path = find_tool_in_custom_paths( custom_paths_str.split(os.pathsep), @@ -390,19 +485,44 @@ def get_ffmpeg_tool_path(tool="ffmpeg"): return tool_executable_path +def get_ffmpeg_tool_args(tool_name, *extra_args): + """Arguments to launch FFmpeg tool. + + Args: + tool_name (str): Tool name 'ffmpeg', 'ffprobe', exc. + *extra_args (str): Extra arguments to add to after tool arguments. + + Returns: + list[str]: List of arguments. + """ + + extra_args = list(extra_args) + + if AYON_SERVER_ENABLED: + args = _get_ayon_ffmpeg_tool_args(tool_name) + if args: + return args + extra_args + + executable_path = get_ffmpeg_tool_path(tool_name) + if executable_path: + return [executable_path] + extra_args + raise ToolNotFoundError( + "FFmpeg '{}' tool not found.".format(tool_name) + ) + + def is_oiio_supported(): """Checks if oiiotool is configured for this platform. Returns: bool: OIIO tool executable is available. """ - loaded_path = oiio_path = get_oiio_tools_path() - if oiio_path: - oiio_path = find_executable(oiio_path) - if not oiio_path: - log.debug("OIIOTool is not configured or not present at {}".format( - loaded_path - )) + try: + args = get_oiio_tool_args("oiiotool") + except ToolNotFoundError: + args = None + if not args: + log.debug("OIIOTool is not configured or not present.") return False - return True + return _oiio_executable_validation(args) diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index e19b7eeb13..4f17731452 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -1,7 +1,7 @@ import os import pyblish from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, run_subprocess ) import tempfile @@ -20,9 +20,6 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): label = "Extract OTIO Audio Tracks" hosts = ["hiero", "resolve", "flame"] - # FFmpeg tools paths - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - def process(self, context): """Convert otio audio track's content to audio representations @@ -91,13 +88,13 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # temp audio file audio_fpath = self.create_temp_file(name) - cmd = [ - self.ffmpeg_path, + cmd = get_ffmpeg_tool_args( + "ffmpeg", "-ss", str(start_sec), "-t", str(duration_sec), "-i", audio_file, audio_fpath - ] + ) # run subprocess self.log.debug("Executing: {}".format(" ".join(cmd))) @@ -210,13 +207,13 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): max_duration_sec = max(end_secs) # create empty cmd - cmd = [ - self.ffmpeg_path, + cmd = get_ffmpeg_tool_args( + "ffmpeg", "-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=48000", "-t", str(max_duration_sec), empty_fpath - ] + ) # generate empty with ffmpeg # run subprocess @@ -295,7 +292,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): filters_tmp_filepath = tmp_file.name tmp_file.write(",".join(filters)) - args = [self.ffmpeg_path] + args = get_ffmpeg_tool_args("ffmpeg") args.extend(input_args) args.extend([ "-filter_complex_script", filters_tmp_filepath, diff --git a/openpype/plugins/publish/extract_otio_review.py b/openpype/plugins/publish/extract_otio_review.py index 9ebcad2af1..699207df8a 100644 --- a/openpype/plugins/publish/extract_otio_review.py +++ b/openpype/plugins/publish/extract_otio_review.py @@ -20,7 +20,7 @@ import opentimelineio as otio from pyblish import api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, run_subprocess, ) from openpype.pipeline import publish @@ -338,8 +338,6 @@ class ExtractOTIOReview(publish.Extractor): Returns: otio.time.TimeRange: trimmed available range """ - # get rendering app path - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") # create path and frame start to destination output_path, out_frame_start = self._get_ffmpeg_output() @@ -348,7 +346,7 @@ class ExtractOTIOReview(publish.Extractor): out_frame_start += end_offset # start command list - command = [ffmpeg_path] + command = get_ffmpeg_tool_args("ffmpeg") input_extension = None if sequence: diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index 70726338aa..67ff6c538c 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -11,7 +11,7 @@ from copy import deepcopy import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, run_subprocess, ) from openpype.pipeline import publish @@ -75,14 +75,12 @@ class ExtractOTIOTrimmingVideo(publish.Extractor): otio_range (opentime.TimeRange): range to trim to """ - # get rendering app path - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") # create path to destination output_path = self._get_ffmpeg_output(input_file_path) # start command list - command = [ffmpeg_path] + command = get_ffmpeg_tool_args("ffmpeg") video_path = input_file_path frame_start = otio_range.start_time.value diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index f053d1b500..9cc456872e 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -3,6 +3,7 @@ import re import copy import json import shutil +import subprocess from abc import ABCMeta, abstractmethod import six @@ -11,7 +12,7 @@ import speedcopy import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, filter_profiles, path_to_subprocess_arg, run_subprocess, @@ -72,9 +73,6 @@ class ExtractReview(pyblish.api.InstancePlugin): alpha_exts = ["exr", "png", "dpx"] - # FFmpeg tools paths - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - # Preset attributes profiles = None @@ -787,8 +785,9 @@ class ExtractReview(pyblish.api.InstancePlugin): arg = arg.replace(identifier, "").strip() audio_filters.append(arg) - all_args = [] - all_args.append(path_to_subprocess_arg(self.ffmpeg_path)) + all_args = [ + subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")) + ] all_args.extend(input_args) if video_filters: all_args.append("-filter:v") diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index fca3d96ca6..8f31f10c42 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -1,5 +1,6 @@ import os import re +import subprocess from pprint import pformat import pyblish.api @@ -7,7 +8,7 @@ import pyblish.api from openpype.lib import ( path_to_subprocess_arg, run_subprocess, - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, get_ffprobe_data, get_ffprobe_streams, get_ffmpeg_codec_args, @@ -47,8 +48,6 @@ class ExtractReviewSlate(publish.Extractor): self.log.info("_ slates_data: {}".format(pformat(slates_data))) - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - if "reviewToWidth" in inst_data: use_legacy_code = True else: @@ -260,7 +259,7 @@ class ExtractReviewSlate(publish.Extractor): _remove_at_end.append(slate_v_path) slate_args = [ - path_to_subprocess_arg(ffmpeg_path), + subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")), " ".join(input_args), " ".join(output_args) ] @@ -281,7 +280,6 @@ class ExtractReviewSlate(publish.Extractor): os.path.splitext(slate_v_path)) _remove_at_end.append(slate_silent_path) self._create_silent_slate( - ffmpeg_path, slate_v_path, slate_silent_path, audio_codec, @@ -309,12 +307,12 @@ class ExtractReviewSlate(publish.Extractor): "[0:v] [1:v] concat=n=2:v=1:a=0 [v]", "-map", '[v]' ] - concat_args = [ - ffmpeg_path, + concat_args = get_ffmpeg_tool_args( + "ffmpeg", "-y", "-i", slate_v_path, "-i", input_path, - ] + ) concat_args.extend(fmap) if offset_timecode: concat_args.extend(["-timecode", offset_timecode]) @@ -490,7 +488,6 @@ class ExtractReviewSlate(publish.Extractor): def _create_silent_slate( self, - ffmpeg_path, src_path, dst_path, audio_codec, @@ -515,8 +512,8 @@ class ExtractReviewSlate(publish.Extractor): one_frame_duration = str(int(one_frame_duration)) + "us" self.log.debug("One frame duration is {}".format(one_frame_duration)) - slate_silent_args = [ - ffmpeg_path, + slate_silent_args = get_ffmpeg_tool_args( + "ffmpeg", "-i", src_path, "-f", "lavfi", "-i", "anullsrc=r={}:cl={}:d={}".format( @@ -531,7 +528,7 @@ class ExtractReviewSlate(publish.Extractor): "-shortest", "-y", dst_path - ] + ) # run slate generation subprocess self.log.debug("Silent Slate Executing: {}".format( " ".join(slate_silent_args) diff --git a/openpype/plugins/publish/extract_scanline_exr.py b/openpype/plugins/publish/extract_scanline_exr.py index 0e4c0ca65f..9f22794a79 100644 --- a/openpype/plugins/publish/extract_scanline_exr.py +++ b/openpype/plugins/publish/extract_scanline_exr.py @@ -5,7 +5,12 @@ import shutil import pyblish.api -from openpype.lib import run_subprocess, get_oiio_tools_path +from openpype.lib import ( + run_subprocess, + get_oiio_tool_args, + ToolNotFoundError, +) +from openpype.pipeline import KnownPublishError class ExtractScanlineExr(pyblish.api.InstancePlugin): @@ -45,11 +50,11 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin): stagingdir = os.path.normpath(repre.get("stagingDir")) - oiio_tool_path = get_oiio_tools_path() - if not os.path.exists(oiio_tool_path): - self.log.error( - "OIIO tool not found in {}".format(oiio_tool_path)) - raise AssertionError("OIIO tool not found") + try: + oiio_tool_args = get_oiio_tool_args("oiiotool") + except ToolNotFoundError: + self.log.error("OIIO tool not found.") + raise KnownPublishError("OIIO tool not found") for file in input_files: @@ -57,8 +62,7 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin): temp_name = os.path.join(stagingdir, "__{}".format(file)) # move original render to temp location shutil.move(original_name, temp_name) - oiio_cmd = [ - oiio_tool_path, + oiio_cmd = oiio_tool_args + [ os.path.join(stagingdir, temp_name), "--scanline", "-o", os.path.join(stagingdir, original_name) ] diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index b98ab64f56..b72a6d02ad 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -1,10 +1,11 @@ import os +import subprocess import tempfile import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, - get_oiio_tools_path, + get_ffmpeg_tool_args, + get_oiio_tool_args, is_oiio_supported, run_subprocess, @@ -174,12 +175,11 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def create_thumbnail_oiio(self, src_path, dst_path): self.log.info("Extracting thumbnail {}".format(dst_path)) - oiio_tool_path = get_oiio_tools_path() - oiio_cmd = [ - oiio_tool_path, + oiio_cmd = get_oiio_tool_args( + "oiiotool", "-a", src_path, "-o", dst_path - ] + ) self.log.debug("running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) @@ -194,27 +194,27 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def create_thumbnail_ffmpeg(self, src_path, dst_path): self.log.info("outputting {}".format(dst_path)) - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") + ffmpeg_path_args = get_ffmpeg_tool_args("ffmpeg") ffmpeg_args = self.ffmpeg_args or {} - jpeg_items = [] - jpeg_items.append(path_to_subprocess_arg(ffmpeg_path)) - # override file if already exists - jpeg_items.append("-y") + jpeg_items = [ + subprocess.list2cmdline(ffmpeg_path_args) + ] # flag for large file sizes max_int = 2147483647 - jpeg_items.append("-analyzeduration {}".format(max_int)) - jpeg_items.append("-probesize {}".format(max_int)) + jpeg_items.extend([ + "-y", + "-analyzeduration", str(max_int), + "-probesize", str(max_int), + ]) # use same input args like with mov jpeg_items.extend(ffmpeg_args.get("input") or []) # input file - jpeg_items.append("-i {}".format( - path_to_subprocess_arg(src_path) - )) + jpeg_items.extend(["-i", path_to_subprocess_arg(src_path)]) # output arguments from presets jpeg_items.extend(ffmpeg_args.get("output") or []) # we just want one frame from movie files - jpeg_items.append("-vframes 1") + jpeg_items.extend(["-vframes", "1"]) # output file jpeg_items.append(path_to_subprocess_arg(dst_path)) subprocess_command = " ".join(jpeg_items) diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index a9c95d6065..54622bb84e 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -17,8 +17,8 @@ import tempfile import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, - get_oiio_tools_path, + get_ffmpeg_tool_args, + get_oiio_tool_args, is_oiio_supported, run_subprocess, @@ -144,12 +144,11 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): def create_thumbnail_oiio(self, src_path, dst_path): self.log.info("outputting {}".format(dst_path)) - oiio_tool_path = get_oiio_tools_path() - oiio_cmd = [ - oiio_tool_path, + oiio_cmd = get_oiio_tool_args( + "oiiotool", "-a", src_path, "-o", dst_path - ] + ) self.log.info("Running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) @@ -162,18 +161,16 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return False def create_thumbnail_ffmpeg(self, src_path, dst_path): - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - max_int = str(2147483647) - ffmpeg_cmd = [ - ffmpeg_path, + ffmpeg_cmd = get_ffmpeg_tool_args( + "ffmpeg", "-y", "-analyzeduration", max_int, "-probesize", max_int, "-i", src_path, "-vframes", "1", dst_path - ] + ) self.log.info("Running: {}".format(" ".join(ffmpeg_cmd))) try: diff --git a/openpype/plugins/publish/extract_trim_video_audio.py b/openpype/plugins/publish/extract_trim_video_audio.py index b951136391..2907ae1839 100644 --- a/openpype/plugins/publish/extract_trim_video_audio.py +++ b/openpype/plugins/publish/extract_trim_video_audio.py @@ -4,7 +4,7 @@ from pprint import pformat import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, run_subprocess, ) from openpype.pipeline import publish @@ -32,7 +32,7 @@ class ExtractTrimVideoAudio(publish.Extractor): instance.data["representations"] = list() # get ffmpet path - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") + ffmpeg_tool_args = get_ffmpeg_tool_args("ffmpeg") # get staging dir staging_dir = self.staging_dir(instance) @@ -76,8 +76,7 @@ class ExtractTrimVideoAudio(publish.Extractor): if "trimming" not in fml ] - ffmpeg_args = [ - ffmpeg_path, + ffmpeg_args = ffmpeg_tool_args + [ "-ss", str(clip_start_h / fps), "-i", video_file_path, "-t", str(clip_dur_h / fps) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 085b62501c..189feaee3a 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -8,21 +8,15 @@ from string import Formatter import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, get_ffmpeg_codec_args, get_ffmpeg_format_args, convert_ffprobe_fps_value, - convert_ffprobe_fps_to_float, ) - -ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") -ffprobe_path = get_ffmpeg_tool_path("ffprobe") - - FFMPEG = ( - '"{}"%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s' -).format(ffmpeg_path) + '{}%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s' +).format(subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg"))) DRAWTEXT = ( "drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':" @@ -46,14 +40,14 @@ def _get_ffprobe_data(source): :param str source: source media file :rtype: [{}, ...] """ - command = [ - ffprobe_path, + command = get_ffmpeg_tool_args( + "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", source - ] + ) kwargs = { "stdout": subprocess.PIPE, } diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index b17ca0adc8..80d156185b 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -7,8 +7,8 @@ from openpype.style import get_objected_colors from openpype.lib import ( run_subprocess, is_oiio_supported, - get_oiio_tools_path, - get_ffmpeg_tool_path, + get_oiio_tool_args, + get_ffmpeg_tool_args, ) from openpype.lib.transcoding import ( IMAGE_EXTENSIONS, @@ -481,12 +481,12 @@ def _convert_thumbnail_oiio(src_path, dst_path): if not is_oiio_supported(): return None - oiio_cmd = [ - get_oiio_tools_path(), + oiio_cmd = get_oiio_tool_args( + "oiiotool", "-i", src_path, "--subimage", "0", "-o", dst_path - ] + ) try: _run_silent_subprocess(oiio_cmd) except Exception: @@ -495,12 +495,12 @@ def _convert_thumbnail_oiio(src_path, dst_path): def _convert_thumbnail_ffmpeg(src_path, dst_path): - ffmpeg_cmd = [ - get_ffmpeg_tool_path(), + ffmpeg_cmd = get_ffmpeg_tool_args( + "ffmpeg", "-y", "-i", src_path, dst_path - ] + ) try: _run_silent_subprocess(ffmpeg_cmd) except Exception: diff --git a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py index f46e31786c..306c43e85d 100644 --- a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -5,6 +5,8 @@ import clique import subprocess import openpype.lib from qtpy import QtWidgets, QtCore + +from openpype.lib import get_ffprobe_data from . import DropEmpty, ComponentsList, ComponentItem @@ -269,26 +271,8 @@ class DropDataFrame(QtWidgets.QFrame): self._process_data(data) def load_data_with_probe(self, filepath): - ffprobe_path = openpype.lib.get_ffmpeg_tool_path("ffprobe") - args = [ - "\"{}\"".format(ffprobe_path), - '-v', 'quiet', - '-print_format json', - '-show_format', - '-show_streams', - '"{}"'.format(filepath) - ] - ffprobe_p = subprocess.Popen( - ' '.join(args), - stdout=subprocess.PIPE, - shell=True - ) - ffprobe_output = ffprobe_p.communicate()[0] - if ffprobe_p.returncode != 0: - raise RuntimeError( - 'Failed on ffprobe: check if ffprobe path is set in PATH env' - ) - return json.loads(ffprobe_output)['streams'][0] + ffprobe_data = get_ffprobe_data(filepath) + return ffprobe_data["streams"][0] def get_file_data(self, data): filepath = data['files'][0] From 84d5c1681cc325f99cac3d5672ec41b9001e3b85 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 27 Jul 2023 14:41:57 +0200 Subject: [PATCH 379/446] Copy file_handler as it will be removed by purging ayon code (#5357) Ayon code will get purged in the future therefore all ayon_common will be gone. file_handler gets internalized to tests as it is not used anywhere else. --- tests/lib/file_handler.py | 289 +++++++++++++++++++++++++++++++++++ tests/lib/testing_classes.py | 2 +- 2 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 tests/lib/file_handler.py diff --git a/tests/lib/file_handler.py b/tests/lib/file_handler.py new file mode 100644 index 0000000000..07f6962c98 --- /dev/null +++ b/tests/lib/file_handler.py @@ -0,0 +1,289 @@ +import os +import re +import urllib +from urllib.parse import urlparse +import urllib.request +import urllib.error +import itertools +import hashlib +import tarfile +import zipfile + +import requests + +USER_AGENT = "AYON-launcher" + + +class RemoteFileHandler: + """Download file from url, might be GDrive shareable link""" + + IMPLEMENTED_ZIP_FORMATS = { + "zip", "tar", "tgz", "tar.gz", "tar.xz", "tar.bz2" + } + + @staticmethod + def calculate_md5(fpath, chunk_size=10000): + md5 = hashlib.md5() + with open(fpath, "rb") as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5.update(chunk) + return md5.hexdigest() + + @staticmethod + def check_md5(fpath, md5, **kwargs): + return md5 == RemoteFileHandler.calculate_md5(fpath, **kwargs) + + @staticmethod + def calculate_sha256(fpath): + """Calculate sha256 for content of the file. + + Args: + fpath (str): Path to file. + + Returns: + str: hex encoded sha256 + + """ + h = hashlib.sha256() + b = bytearray(128 * 1024) + mv = memoryview(b) + with open(fpath, "rb", buffering=0) as f: + for n in iter(lambda: f.readinto(mv), 0): + h.update(mv[:n]) + return h.hexdigest() + + @staticmethod + def check_sha256(fpath, sha256, **kwargs): + return sha256 == RemoteFileHandler.calculate_sha256(fpath, **kwargs) + + @staticmethod + def check_integrity(fpath, hash_value=None, hash_type=None): + if not os.path.isfile(fpath): + return False + if hash_value is None: + return True + if not hash_type: + raise ValueError("Provide hash type, md5 or sha256") + if hash_type == "md5": + return RemoteFileHandler.check_md5(fpath, hash_value) + if hash_type == "sha256": + return RemoteFileHandler.check_sha256(fpath, hash_value) + + @staticmethod + def download_url( + url, + root, + filename=None, + max_redirect_hops=3, + headers=None + ): + """Download a file from url and place it in root. + + Args: + url (str): URL to download file from + root (str): Directory to place downloaded file in + filename (str, optional): Name to save the file under. + If None, use the basename of the URL + max_redirect_hops (Optional[int]): Maximum number of redirect + hops allowed + headers (Optional[dict[str, str]]): Additional required headers + - Authentication etc.. + """ + + root = os.path.expanduser(root) + if not filename: + filename = os.path.basename(url) + fpath = os.path.join(root, filename) + + os.makedirs(root, exist_ok=True) + + # expand redirect chain if needed + url = RemoteFileHandler._get_redirect_url( + url, max_hops=max_redirect_hops, headers=headers) + + # check if file is located on Google Drive + file_id = RemoteFileHandler._get_google_drive_file_id(url) + if file_id is not None: + return RemoteFileHandler.download_file_from_google_drive( + file_id, root, filename) + + # download the file + try: + print(f"Downloading {url} to {fpath}") + RemoteFileHandler._urlretrieve(url, fpath, headers=headers) + except (urllib.error.URLError, IOError) as exc: + if url[:5] != "https": + raise exc + + url = url.replace("https:", "http:") + print(( + "Failed download. Trying https -> http instead." + f" Downloading {url} to {fpath}" + )) + RemoteFileHandler._urlretrieve(url, fpath, headers=headers) + + @staticmethod + def download_file_from_google_drive( + file_id, root, filename=None + ): + """Download a Google Drive file from and place it in root. + Args: + file_id (str): id of file to be downloaded + root (str): Directory to place downloaded file in + filename (str, optional): Name to save the file under. + If None, use the id of the file. + """ + # Based on https://stackoverflow.com/questions/38511444/python-download-files-from-google-drive-using-url # noqa + + url = "https://docs.google.com/uc?export=download" + + root = os.path.expanduser(root) + if not filename: + filename = file_id + fpath = os.path.join(root, filename) + + os.makedirs(root, exist_ok=True) + + if os.path.isfile(fpath) and RemoteFileHandler.check_integrity(fpath): + print(f"Using downloaded and verified file: {fpath}") + else: + session = requests.Session() + + response = session.get(url, params={"id": file_id}, stream=True) + token = RemoteFileHandler._get_confirm_token(response) + + if token: + params = {"id": file_id, "confirm": token} + response = session.get(url, params=params, stream=True) + + response_content_generator = response.iter_content(32768) + first_chunk = None + while not first_chunk: # filter out keep-alive new chunks + first_chunk = next(response_content_generator) + + if RemoteFileHandler._quota_exceeded(first_chunk): + msg = ( + f"The daily quota of the file {filename} is exceeded and " + f"it can't be downloaded. This is a limitation of " + f"Google Drive and can only be overcome by trying " + f"again later." + ) + raise RuntimeError(msg) + + RemoteFileHandler._save_response_content( + itertools.chain((first_chunk, ), + response_content_generator), fpath) + response.close() + + @staticmethod + def unzip(path, destination_path=None): + if not destination_path: + destination_path = os.path.dirname(path) + + _, archive_type = os.path.splitext(path) + archive_type = archive_type.lstrip(".") + + if archive_type in ["zip"]: + print(f"Unzipping {path}->{destination_path}") + zip_file = zipfile.ZipFile(path) + zip_file.extractall(destination_path) + zip_file.close() + + elif archive_type in [ + "tar", "tgz", "tar.gz", "tar.xz", "tar.bz2" + ]: + print(f"Unzipping {path}->{destination_path}") + if archive_type == "tar": + tar_type = "r:" + elif archive_type.endswith("xz"): + tar_type = "r:xz" + elif archive_type.endswith("gz"): + tar_type = "r:gz" + elif archive_type.endswith("bz2"): + tar_type = "r:bz2" + else: + tar_type = "r:*" + try: + tar_file = tarfile.open(path, tar_type) + except tarfile.ReadError: + raise SystemExit("corrupted archive") + tar_file.extractall(destination_path) + tar_file.close() + + @staticmethod + def _urlretrieve(url, filename, chunk_size=None, headers=None): + final_headers = {"User-Agent": USER_AGENT} + if headers: + final_headers.update(headers) + + chunk_size = chunk_size or 8192 + with open(filename, "wb") as fh: + with urllib.request.urlopen( + urllib.request.Request(url, headers=final_headers) + ) as response: + for chunk in iter(lambda: response.read(chunk_size), ""): + if not chunk: + break + fh.write(chunk) + + @staticmethod + def _get_redirect_url(url, max_hops, headers=None): + initial_url = url + final_headers = {"Method": "HEAD", "User-Agent": USER_AGENT} + if headers: + final_headers.update(headers) + for _ in range(max_hops + 1): + with urllib.request.urlopen( + urllib.request.Request(url, headers=final_headers) + ) as response: + if response.url == url or response.url is None: + return url + + return response.url + else: + raise RecursionError( + f"Request to {initial_url} exceeded {max_hops} redirects. " + f"The last redirect points to {url}." + ) + + @staticmethod + def _get_confirm_token(response): + for key, value in response.cookies.items(): + if key.startswith("download_warning"): + return value + + # handle antivirus warning for big zips + found = re.search("(confirm=)([^&.+])", response.text) + if found: + return found.groups()[1] + + return None + + @staticmethod + def _save_response_content( + response_gen, destination, + ): + with open(destination, "wb") as f: + for chunk in response_gen: + if chunk: # filter out keep-alive new chunks + f.write(chunk) + + @staticmethod + def _quota_exceeded(first_chunk): + try: + return "Google Drive - Quota exceeded" in first_chunk.decode() + except UnicodeDecodeError: + return False + + @staticmethod + def _get_google_drive_file_id(url): + parts = urlparse(url) + + if re.match(r"(drive|docs)[.]google[.]com", parts.netloc) is None: + return None + + match = re.match(r"/file/d/(?P[^/]*)", parts.path) + if match is None: + return None + + return match.group("id") diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index f04607dc27..2af4af02de 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -12,7 +12,7 @@ import requests import re from tests.lib.db_handler import DBHandler -from common.ayon_common.distribution.file_handler import RemoteFileHandler +from tests.lib.file_handler import RemoteFileHandler from openpype.modules import ModulesManager from openpype.settings import get_project_settings From a9eaa68ac60e783691806d172056251305f4a961 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 27 Jul 2023 14:42:21 +0200 Subject: [PATCH 380/446] AYON: Remove AYON launch logic from OpenPype (#5348) * removed AYON launch logic from OpenPype * updated ayon api to 0.3.3 * removed common from include files --------- Co-authored-by: 64qam --- ayon_start.py | 483 ------- common/ayon_common/__init__.py | 16 - common/ayon_common/connection/__init__.py | 0 common/ayon_common/connection/credentials.py | 511 -------- common/ayon_common/connection/ui/__init__.py | 12 - common/ayon_common/connection/ui/__main__.py | 23 - .../ayon_common/connection/ui/login_window.py | 710 ----------- common/ayon_common/connection/ui/widgets.py | 47 - common/ayon_common/distribution/README.md | 18 - common/ayon_common/distribution/__init__.py | 9 - common/ayon_common/distribution/control.py | 1116 ----------------- .../distribution/data_structures.py | 265 ---- .../ayon_common/distribution/downloaders.py | 250 ---- .../ayon_common/distribution/file_handler.py | 289 ----- .../tests/test_addon_distributtion.py | 248 ---- .../distribution/ui/missing_bundle_window.py | 146 --- common/ayon_common/distribution/utils.py | 90 -- common/ayon_common/resources/AYON.icns | Bin 40634 -> 0 bytes common/ayon_common/resources/AYON.ico | Bin 4286 -> 0 bytes common/ayon_common/resources/AYON.png | Bin 16907 -> 0 bytes common/ayon_common/resources/AYON_staging.png | Bin 15273 -> 0 bytes common/ayon_common/resources/__init__.py | 25 - common/ayon_common/resources/edit.png | Bin 9138 -> 0 bytes common/ayon_common/resources/eye.png | Bin 2152 -> 0 bytes common/ayon_common/resources/stylesheet.css | 84 -- common/ayon_common/ui_utils.py | 36 - common/ayon_common/utils.py | 90 -- .../vendor/python/common/ayon_api/__init__.py | 4 + .../vendor/python/common/ayon_api/_api.py | 64 +- .../python/common/ayon_api/server_api.py | 163 ++- .../python/common/ayon_api/thumbnails.py | 2 +- .../vendor/python/common/ayon_api/utils.py | 139 +- .../vendor/python/common/ayon_api/version.py | 2 +- setup.py | 17 - tools/run_tray_ayon.ps1 | 41 - tools/run_tray_ayon.sh | 78 -- 36 files changed, 321 insertions(+), 4657 deletions(-) delete mode 100644 ayon_start.py delete mode 100644 common/ayon_common/__init__.py delete mode 100644 common/ayon_common/connection/__init__.py delete mode 100644 common/ayon_common/connection/credentials.py delete mode 100644 common/ayon_common/connection/ui/__init__.py delete mode 100644 common/ayon_common/connection/ui/__main__.py delete mode 100644 common/ayon_common/connection/ui/login_window.py delete mode 100644 common/ayon_common/connection/ui/widgets.py delete mode 100644 common/ayon_common/distribution/README.md delete mode 100644 common/ayon_common/distribution/__init__.py delete mode 100644 common/ayon_common/distribution/control.py delete mode 100644 common/ayon_common/distribution/data_structures.py delete mode 100644 common/ayon_common/distribution/downloaders.py delete mode 100644 common/ayon_common/distribution/file_handler.py delete mode 100644 common/ayon_common/distribution/tests/test_addon_distributtion.py delete mode 100644 common/ayon_common/distribution/ui/missing_bundle_window.py delete mode 100644 common/ayon_common/distribution/utils.py delete mode 100644 common/ayon_common/resources/AYON.icns delete mode 100644 common/ayon_common/resources/AYON.ico delete mode 100644 common/ayon_common/resources/AYON.png delete mode 100644 common/ayon_common/resources/AYON_staging.png delete mode 100644 common/ayon_common/resources/__init__.py delete mode 100644 common/ayon_common/resources/edit.png delete mode 100644 common/ayon_common/resources/eye.png delete mode 100644 common/ayon_common/resources/stylesheet.css delete mode 100644 common/ayon_common/ui_utils.py delete mode 100644 common/ayon_common/utils.py delete mode 100644 tools/run_tray_ayon.ps1 delete mode 100755 tools/run_tray_ayon.sh diff --git a/ayon_start.py b/ayon_start.py deleted file mode 100644 index 458c46bba6..0000000000 --- a/ayon_start.py +++ /dev/null @@ -1,483 +0,0 @@ -# -*- coding: utf-8 -*- -"""Main entry point for AYON command. - -Bootstrapping process of AYON. -""" -import os -import sys -import site -import traceback -import contextlib - - -# Enabled logging debug mode when "--debug" is passed -if "--verbose" in sys.argv: - expected_values = ( - "Expected: notset, debug, info, warning, error, critical" - " or integer [0-50]." - ) - idx = sys.argv.index("--verbose") - sys.argv.pop(idx) - if idx < len(sys.argv): - value = sys.argv.pop(idx) - else: - raise RuntimeError(( - f"Expect value after \"--verbose\" argument. {expected_values}" - )) - - log_level = None - low_value = value.lower() - if low_value.isdigit(): - log_level = int(low_value) - elif low_value == "notset": - log_level = 0 - elif low_value == "debug": - log_level = 10 - elif low_value == "info": - log_level = 20 - elif low_value == "warning": - log_level = 30 - elif low_value == "error": - log_level = 40 - elif low_value == "critical": - log_level = 50 - - if log_level is None: - raise ValueError(( - "Unexpected value after \"--verbose\" " - f"argument \"{value}\". {expected_values}" - )) - - os.environ["OPENPYPE_LOG_LEVEL"] = str(log_level) - os.environ["AYON_LOG_LEVEL"] = str(log_level) - -# Enable debug mode, may affect log level if log level is not defined -if "--debug" in sys.argv: - sys.argv.remove("--debug") - os.environ["AYON_DEBUG"] = "1" - os.environ["OPENPYPE_DEBUG"] = "1" - -if "--automatic-tests" in sys.argv: - sys.argv.remove("--automatic-tests") - os.environ["IS_TEST"] = "1" - -SKIP_HEADERS = False -if "--skip-headers" in sys.argv: - sys.argv.remove("--skip-headers") - SKIP_HEADERS = True - -SKIP_BOOTSTRAP = False -if "--skip-bootstrap" in sys.argv: - sys.argv.remove("--skip-bootstrap") - SKIP_BOOTSTRAP = True - -if "--use-staging" in sys.argv: - sys.argv.remove("--use-staging") - os.environ["AYON_USE_STAGING"] = "1" - os.environ["OPENPYPE_USE_STAGING"] = "1" - -if "--headless" in sys.argv: - os.environ["AYON_HEADLESS_MODE"] = "1" - os.environ["OPENPYPE_HEADLESS_MODE"] = "1" - sys.argv.remove("--headless") - -elif ( - os.getenv("AYON_HEADLESS_MODE") != "1" - or os.getenv("OPENPYPE_HEADLESS_MODE") != "1" -): - os.environ.pop("AYON_HEADLESS_MODE", None) - os.environ.pop("OPENPYPE_HEADLESS_MODE", None) - -elif ( - os.getenv("AYON_HEADLESS_MODE") - != os.getenv("OPENPYPE_HEADLESS_MODE") -): - os.environ["OPENPYPE_HEADLESS_MODE"] = ( - os.environ["AYON_HEADLESS_MODE"] - ) - -IS_BUILT_APPLICATION = getattr(sys, "frozen", False) -HEADLESS_MODE_ENABLED = os.getenv("AYON_HEADLESS_MODE") == "1" - -_pythonpath = os.getenv("PYTHONPATH", "") -_python_paths = _pythonpath.split(os.pathsep) -if not IS_BUILT_APPLICATION: - # Code root defined by `start.py` directory - AYON_ROOT = os.path.dirname(os.path.abspath(__file__)) - _dependencies_path = site.getsitepackages()[-1] -else: - AYON_ROOT = os.path.dirname(sys.executable) - - # add dependencies folder to sys.pat for frozen code - _dependencies_path = os.path.normpath( - os.path.join(AYON_ROOT, "dependencies") - ) -# add stuff from `/dependencies` to PYTHONPATH. -sys.path.append(_dependencies_path) -_python_paths.append(_dependencies_path) - -# Vendored python modules that must not be in PYTHONPATH environment but -# are required for OpenPype processes -sys.path.insert(0, os.path.join(AYON_ROOT, "vendor", "python")) - -# Add common package to sys path -# - common contains common code for bootstraping and OpenPype processes -sys.path.insert(0, os.path.join(AYON_ROOT, "common")) - -# This is content of 'core' addon which is ATM part of build -common_python_vendor = os.path.join( - AYON_ROOT, - "openpype", - "vendor", - "python", - "common" -) -# Add tools dir to sys path for pyblish UI discovery -tools_dir = os.path.join(AYON_ROOT, "openpype", "tools") -for path in (AYON_ROOT, common_python_vendor, tools_dir): - while path in _python_paths: - _python_paths.remove(path) - - while path in sys.path: - sys.path.remove(path) - - _python_paths.insert(0, path) - sys.path.insert(0, path) - -os.environ["PYTHONPATH"] = os.pathsep.join(_python_paths) - -# enabled AYON state -os.environ["USE_AYON_SERVER"] = "1" -# Set this to point either to `python` from venv in case of live code -# or to `ayon` or `ayon_console` in case of frozen code -os.environ["AYON_EXECUTABLE"] = sys.executable -os.environ["OPENPYPE_EXECUTABLE"] = sys.executable -os.environ["AYON_ROOT"] = AYON_ROOT -os.environ["OPENPYPE_ROOT"] = AYON_ROOT -os.environ["OPENPYPE_REPOS_ROOT"] = AYON_ROOT -os.environ["AYON_MENU_LABEL"] = "AYON" -os.environ["AVALON_LABEL"] = "AYON" -# Set name of pyblish UI import -os.environ["PYBLISH_GUI"] = "pyblish_pype" -# Set builtin OCIO root -os.environ["BUILTIN_OCIO_ROOT"] = os.path.join( - AYON_ROOT, - "vendor", - "bin", - "ocioconfig", - "OpenColorIOConfigs" -) - -import blessed # noqa: E402 -import certifi # noqa: E402 - - -if sys.__stdout__: - term = blessed.Terminal() - - def _print(message: str): - if message.startswith("!!! "): - print(f'{term.orangered2("!!! ")}{message[4:]}') - elif message.startswith(">>> "): - print(f'{term.aquamarine3(">>> ")}{message[4:]}') - elif message.startswith("--- "): - print(f'{term.darkolivegreen3("--- ")}{message[4:]}') - elif message.startswith("*** "): - print(f'{term.gold("*** ")}{message[4:]}') - elif message.startswith(" - "): - print(f'{term.wheat(" - ")}{message[4:]}') - elif message.startswith(" . "): - print(f'{term.tan(" . ")}{message[4:]}') - elif message.startswith(" - "): - print(f'{term.seagreen3(" - ")}{message[7:]}') - elif message.startswith(" ! "): - print(f'{term.goldenrod(" ! ")}{message[7:]}') - elif message.startswith(" * "): - print(f'{term.aquamarine1(" * ")}{message[7:]}') - elif message.startswith(" "): - print(f'{term.darkseagreen3(" ")}{message[4:]}') - else: - print(message) -else: - def _print(message: str): - print(message) - - -# if SSL_CERT_FILE is not set prior to OpenPype launch, we set it to point -# to certifi bundle to make sure we have reasonably new CA certificates. -if not os.getenv("SSL_CERT_FILE"): - os.environ["SSL_CERT_FILE"] = certifi.where() -elif os.getenv("SSL_CERT_FILE") != certifi.where(): - _print("--- your system is set to use custom CA certificate bundle.") - -from ayon_api import get_base_url -from ayon_api.constants import SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY -from ayon_common import is_staging_enabled -from ayon_common.connection.credentials import ( - ask_to_login_ui, - add_server, - need_server_or_login, - load_environments, - set_environments, - create_global_connection, - confirm_server_login, -) -from ayon_common.distribution import ( - AyonDistribution, - BundleNotFoundError, - show_missing_bundle_information, -) - - -def set_global_environments() -> None: - """Set global OpenPype's environments.""" - import acre - - from openpype.settings import get_general_environments - - general_env = get_general_environments() - - # first resolve general environment because merge doesn't expect - # values to be list. - # TODO: switch to OpenPype environment functions - merged_env = acre.merge( - acre.compute(acre.parse(general_env), cleanup=False), - dict(os.environ) - ) - env = acre.compute( - merged_env, - cleanup=False - ) - os.environ.clear() - os.environ.update(env) - - # Hardcoded default values - os.environ["PYBLISH_GUI"] = "pyblish_pype" - # Change scale factor only if is not set - if "QT_AUTO_SCREEN_SCALE_FACTOR" not in os.environ: - os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" - - -def set_addons_environments(): - """Set global environments for OpenPype modules. - - This requires to have OpenPype in `sys.path`. - """ - - import acre - from openpype.modules import ModulesManager - - modules_manager = ModulesManager() - - # Merge environments with current environments and update values - if module_envs := modules_manager.collect_global_environments(): - parsed_envs = acre.parse(module_envs) - env = acre.merge(parsed_envs, dict(os.environ)) - os.environ.clear() - os.environ.update(env) - - -def _connect_to_ayon_server(): - load_environments() - if not need_server_or_login(): - create_global_connection() - return - - if HEADLESS_MODE_ENABLED: - _print("!!! Cannot open v4 Login dialog in headless mode.") - _print(( - "!!! Please use `{}` to specify server address" - " and '{}' to specify user's token." - ).format(SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY)) - sys.exit(1) - - current_url = os.environ.get(SERVER_URL_ENV_KEY) - url, token, username = ask_to_login_ui(current_url, always_on_top=True) - if url is not None and token is not None: - confirm_server_login(url, token, username) - return - - if url is not None: - add_server(url, username) - - _print("!!! Login was not successful.") - sys.exit(0) - - -def _check_and_update_from_ayon_server(): - """Gets addon info from v4, compares with local folder and updates it. - - Raises: - RuntimeError - """ - - distribution = AyonDistribution() - bundle = None - bundle_name = None - try: - bundle = distribution.bundle_to_use - if bundle is not None: - bundle_name = bundle.name - except BundleNotFoundError as exc: - bundle_name = exc.bundle_name - - if bundle is None: - url = get_base_url() - if not HEADLESS_MODE_ENABLED: - show_missing_bundle_information(url, bundle_name) - - elif bundle_name: - _print(( - f"!!! Requested release bundle '{bundle_name}'" - " is not available on server." - )) - _print( - "!!! Check if selected release bundle" - f" is available on the server '{url}'." - ) - - else: - mode = "staging" if is_staging_enabled() else "production" - _print( - f"!!! No release bundle is set as {mode} on the AYON server." - ) - _print( - "!!! Make sure there is a release bundle set" - f" as \"{mode}\" on the AYON server '{url}'." - ) - sys.exit(1) - - distribution.distribute() - distribution.validate_distribution() - os.environ["AYON_BUNDLE_NAME"] = bundle_name - - python_paths = [ - path - for path in os.getenv("PYTHONPATH", "").split(os.pathsep) - if path - ] - - for path in distribution.get_sys_paths(): - sys.path.insert(0, path) - if path not in python_paths: - python_paths.append(path) - os.environ["PYTHONPATH"] = os.pathsep.join(python_paths) - - -def boot(): - """Bootstrap OpenPype.""" - - from openpype.version import __version__ - - # TODO load version - os.environ["OPENPYPE_VERSION"] = __version__ - os.environ["AYON_VERSION"] = __version__ - - _connect_to_ayon_server() - _check_and_update_from_ayon_server() - - # delete OpenPype module and it's submodules from cache so it is used from - # specific version - modules_to_del = [ - sys.modules.pop(module_name) - for module_name in tuple(sys.modules) - if module_name == "openpype" or module_name.startswith("openpype.") - ] - - for module_name in modules_to_del: - with contextlib.suppress(AttributeError, KeyError): - del sys.modules[module_name] - - -def main_cli(): - from openpype import cli - from openpype.version import __version__ - from openpype.lib import terminal as t - - _print(">>> loading environments ...") - _print(" - global AYON ...") - set_global_environments() - _print(" - for addons ...") - set_addons_environments() - - # print info when not running scripts defined in 'silent commands' - if not SKIP_HEADERS: - info = get_info(is_staging_enabled()) - info.insert(0, f">>> Using AYON from [ {AYON_ROOT} ]") - - t_width = 20 - with contextlib.suppress(ValueError, OSError): - t_width = os.get_terminal_size().columns - 2 - - _header = f"*** AYON [{__version__}] " - info.insert(0, _header + "-" * (t_width - len(_header))) - - for i in info: - t.echo(i) - - try: - cli.main(obj={}, prog_name="ayon") - except Exception: # noqa - exc_info = sys.exc_info() - _print("!!! AYON crashed:") - traceback.print_exception(*exc_info) - sys.exit(1) - - -def script_cli(): - """Run and execute script.""" - - filepath = os.path.abspath(sys.argv[1]) - - # Find '__main__.py' in directory - if os.path.isdir(filepath): - new_filepath = os.path.join(filepath, "__main__.py") - if not os.path.exists(new_filepath): - raise RuntimeError( - f"can't find '__main__' module in '{filepath}'") - filepath = new_filepath - - # Add parent dir to sys path - sys.path.insert(0, os.path.dirname(filepath)) - - # Read content and execute - with open(filepath, "r") as stream: - content = stream.read() - - exec(compile(content, filepath, "exec"), globals()) - - -def get_info(use_staging=None) -> list: - """Print additional information to console.""" - - inf = [] - if use_staging: - inf.append(("AYON variant", "staging")) - else: - inf.append(("AYON variant", "production")) - inf.append(("AYON bundle", os.getenv("AYON_BUNDLE"))) - - # NOTE add addons information - - maximum = max(len(i[0]) for i in inf) - formatted = [] - for info in inf: - padding = (maximum - len(info[0])) + 1 - formatted.append(f'... {info[0]}:{" " * padding}[ {info[1]} ]') - return formatted - - -def main(): - if not SKIP_BOOTSTRAP: - boot() - - args = list(sys.argv) - args.pop(0) - if args and os.path.exists(args[0]): - script_cli() - else: - main_cli() - - -if __name__ == "__main__": - main() diff --git a/common/ayon_common/__init__.py b/common/ayon_common/__init__.py deleted file mode 100644 index ddabb7da2f..0000000000 --- a/common/ayon_common/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .utils import ( - IS_BUILT_APPLICATION, - is_staging_enabled, - get_local_site_id, - get_ayon_appdirs, - get_ayon_launch_args, -) - - -__all__ = ( - "IS_BUILT_APPLICATION", - "is_staging_enabled", - "get_local_site_id", - "get_ayon_appdirs", - "get_ayon_launch_args", -) diff --git a/common/ayon_common/connection/__init__.py b/common/ayon_common/connection/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/ayon_common/connection/credentials.py b/common/ayon_common/connection/credentials.py deleted file mode 100644 index 7f70cb7992..0000000000 --- a/common/ayon_common/connection/credentials.py +++ /dev/null @@ -1,511 +0,0 @@ -"""Handle credentials and connection to server for client application. - -Cache and store used server urls. Store/load API keys to/from keyring if -needed. Store metadata about used urls, usernames for the urls and when was -the connection with the username established. - -On bootstrap is created global connection with information about site and -client version. The connection object lives in 'ayon_api'. -""" - -import os -import json -import platform -import datetime -import contextlib -import subprocess -import tempfile -from typing import Optional, Union, Any - -import ayon_api - -from ayon_api.constants import SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY -from ayon_api.exceptions import UrlError -from ayon_api.utils import ( - validate_url, - is_token_valid, - logout_from_server, -) - -from ayon_common.utils import ( - get_ayon_appdirs, - get_local_site_id, - get_ayon_launch_args, - is_staging_enabled, -) - - -class ChangeUserResult: - def __init__( - self, logged_out, old_url, old_token, old_username, - new_url, new_token, new_username - ): - shutdown = logged_out - restart = new_url is not None and new_url != old_url - token_changed = new_token is not None and new_token != old_token - - self.logged_out = logged_out - self.old_url = old_url - self.old_token = old_token - self.old_username = old_username - self.new_url = new_url - self.new_token = new_token - self.new_username = new_username - - self.shutdown = shutdown - self.restart = restart - self.token_changed = token_changed - - -def _get_servers_path(): - return get_ayon_appdirs("used_servers.json") - - -def get_servers_info_data(): - """Metadata about used server on this machine. - - Store data about all used server urls, last used url and user username for - the url. Using this metadata we can remember which username was used per - url if token stored in keyring loose lifetime. - - Returns: - dict[str, Any]: Information about servers. - """ - - data = {} - servers_info_path = _get_servers_path() - if not os.path.exists(servers_info_path): - dirpath = os.path.dirname(servers_info_path) - if not os.path.exists(dirpath): - os.makedirs(dirpath) - - return data - - with open(servers_info_path, "r") as stream: - with contextlib.suppress(BaseException): - data = json.load(stream) - return data - - -def add_server(url: str, username: str): - """Add server to server info metadata. - - This function will also mark the url as last used url on the machine so on - next launch will be used. - - Args: - url (str): Server url. - username (str): Name of user used to log in. - """ - - servers_info_path = _get_servers_path() - data = get_servers_info_data() - data["last_server"] = url - if "urls" not in data: - data["urls"] = {} - data["urls"][url] = { - "updated_dt": datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"), - "username": username, - } - - with open(servers_info_path, "w") as stream: - json.dump(data, stream) - - -def remove_server(url: str): - """Remove server url from servers information. - - This should be used on logout to completelly loose information about server - on the machine. - - Args: - url (str): Server url. - """ - - if not url: - return - - servers_info_path = _get_servers_path() - data = get_servers_info_data() - if data.get("last_server") == url: - data["last_server"] = None - - if "urls" in data: - data["urls"].pop(url, None) - - with open(servers_info_path, "w") as stream: - json.dump(data, stream) - - -def get_last_server( - data: Optional[dict[str, Any]] = None -) -> Union[str, None]: - """Last server used to log in on this machine. - - Args: - data (Optional[dict[str, Any]]): Prepared server information data. - - Returns: - Union[str, None]: Last used server url. - """ - - if data is None: - data = get_servers_info_data() - return data.get("last_server") - - -def get_last_username_by_url( - url: str, - data: Optional[dict[str, Any]] = None -) -> Union[str, None]: - """Get last username which was used for passed url. - - Args: - url (str): Server url. - data (Optional[dict[str, Any]]): Servers info. - - Returns: - Union[str, None]: Username. - """ - - if not url: - return None - - if data is None: - data = get_servers_info_data() - - if urls := data.get("urls"): - if url_info := urls.get(url): - return url_info.get("username") - return None - - -def get_last_server_with_username(): - """Receive last server and username used in last connection. - - Returns: - tuple[Union[str, None], Union[str, None]]: Url and username. - """ - - data = get_servers_info_data() - url = get_last_server(data) - username = get_last_username_by_url(url) - return url, username - - -class TokenKeyring: - # Fake username with hardcoded username - username_key = "username" - - def __init__(self, url): - try: - import keyring - - except Exception as exc: - raise NotImplementedError( - "Python module `keyring` is not available." - ) from exc - - # hack for cx_freeze and Windows keyring backend - if platform.system().lower() == "windows": - from keyring.backends import Windows - - keyring.set_keyring(Windows.WinVaultKeyring()) - - self._url = url - self._keyring_key = f"AYON/{url}" - - def get_value(self): - import keyring - - return keyring.get_password(self._keyring_key, self.username_key) - - def set_value(self, value): - import keyring - - if value is not None: - keyring.set_password(self._keyring_key, self.username_key, value) - return - - with contextlib.suppress(keyring.errors.PasswordDeleteError): - keyring.delete_password(self._keyring_key, self.username_key) - - -def load_token(url: str) -> Union[str, None]: - """Get token for url from keyring. - - Args: - url (str): Server url. - - Returns: - Union[str, None]: Token for passed url available in keyring. - """ - - return TokenKeyring(url).get_value() - - -def store_token(url: str, token: str): - """Store token by url to keyring. - - Args: - url (str): Server url. - token (str): User token to server. - """ - - TokenKeyring(url).set_value(token) - - -def ask_to_login_ui( - url: Optional[str] = None, - always_on_top: Optional[bool] = False -) -> tuple[str, str, str]: - """Ask user to login using UI. - - This should be used only when user is not yet logged in at all or available - credentials are invalid. To change credentials use 'change_user_ui' - function. - - Use a subprocess to show UI. - - Args: - url (Optional[str]): Server url that could be prefilled in UI. - always_on_top (Optional[bool]): Window will be drawn on top of - other windows. - - Returns: - tuple[str, str, str]: Url, user's token and username. - """ - - current_dir = os.path.dirname(os.path.abspath(__file__)) - ui_dir = os.path.join(current_dir, "ui") - - if url is None: - url = get_last_server() - username = get_last_username_by_url(url) - data = { - "url": url, - "username": username, - "always_on_top": always_on_top, - } - - with tempfile.NamedTemporaryFile( - mode="w", prefix="ayon_login", suffix=".json", delete=False - ) as tmp: - output = tmp.name - json.dump(data, tmp) - - code = subprocess.call( - get_ayon_launch_args(ui_dir, "--skip-bootstrap", output)) - if code != 0: - raise RuntimeError("Failed to show login UI") - - with open(output, "r") as stream: - data = json.load(stream) - os.remove(output) - return data["output"] - - -def change_user_ui() -> ChangeUserResult: - """Change user using UI. - - Show UI to user where he can change credentials or url. Output will contain - all information about old/new values of url, username, api key. If user - confirmed or declined values. - - Returns: - ChangeUserResult: Information about user change. - """ - - from .ui import change_user - - url, username = get_last_server_with_username() - token = load_token(url) - result = change_user(url, username, token) - new_url, new_token, new_username, logged_out = result - - output = ChangeUserResult( - logged_out, url, token, username, - new_url, new_token, new_username - ) - if output.logged_out: - logout(url, token) - - elif output.token_changed: - change_token( - output.new_url, - output.new_token, - output.new_username, - output.old_url - ) - return output - - -def change_token( - url: str, - token: str, - username: Optional[str] = None, - old_url: Optional[str] = None -): - """Change url and token in currently running session. - - Function can also change server url, in that case are previous credentials - NOT removed from cache. - - Args: - url (str): Url to server. - token (str): New token to be used for url connection. - username (Optional[str]): Username of logged user. - old_url (Optional[str]): Previous url. Value from 'get_last_server' - is used if not entered. - """ - - if old_url is None: - old_url = get_last_server() - if old_url and old_url == url: - remove_url_cache(old_url) - - # TODO check if ayon_api is already connected - add_server(url, username) - store_token(url, token) - ayon_api.change_token(url, token) - - -def remove_url_cache(url: str): - """Clear cache for server url. - - Args: - url (str): Server url which is removed from cache. - """ - - store_token(url, None) - - -def remove_token_cache(url: str, token: str): - """Remove token from local cache of url. - - Is skipped if cached token under the passed url is not the same - as passed token. - - Args: - url (str): Url to server. - token (str): Token to be removed from url cache. - """ - - if load_token(url) == token: - remove_url_cache(url) - - -def logout(url: str, token: str): - """Logout from server and throw token away. - - Args: - url (str): Url from which should be logged out. - token (str): Token which should be used to log out. - """ - - remove_server(url) - ayon_api.close_connection() - ayon_api.set_environments(None, None) - remove_token_cache(url, token) - logout_from_server(url, token) - - -def load_environments(): - """Load environments on startup. - - Handle environments needed for connection with server. Environments are - 'AYON_SERVER_URL' and 'AYON_API_KEY'. - - Server is looked up from environment. Already set environent is not - changed. If environemnt is not filled then last server stored in appdirs - is used. - - Token is skipped if url is not available. Otherwise, is also checked from - env and if is not available then uses 'load_token' to try to get token - based on server url. - """ - - server_url = os.environ.get(SERVER_URL_ENV_KEY) - if not server_url: - server_url = get_last_server() - if not server_url: - return - os.environ[SERVER_URL_ENV_KEY] = server_url - - if not os.environ.get(SERVER_API_ENV_KEY): - if token := load_token(server_url): - os.environ[SERVER_API_ENV_KEY] = token - - -def set_environments(url: str, token: str): - """Change url and token environemnts in currently running process. - - Args: - url (str): New server url. - token (str): User's token. - """ - - ayon_api.set_environments(url, token) - - -def create_global_connection(): - """Create global connection with site id and client version. - - Make sure the global connection in 'ayon_api' have entered site id and - client version. - - Set default settings variant to use based on 'is_staging_enabled'. - """ - - ayon_api.create_connection( - get_local_site_id(), os.environ.get("AYON_VERSION") - ) - ayon_api.set_default_settings_variant( - "staging" if is_staging_enabled() else "production" - ) - - -def need_server_or_login() -> bool: - """Check if server url or login to the server are needed. - - It is recommended to call 'load_environments' on startup before this check. - But in some cases this function could be called after startup. - - Returns: - bool: 'True' if server and token are available. Otherwise 'False'. - """ - - server_url = os.environ.get(SERVER_URL_ENV_KEY) - if not server_url: - return True - - try: - server_url = validate_url(server_url) - except UrlError: - return True - - token = os.environ.get(SERVER_API_ENV_KEY) - if token: - return not is_token_valid(server_url, token) - - token = load_token(server_url) - if token: - return not is_token_valid(server_url, token) - return True - - -def confirm_server_login(url, token, username): - """Confirm login of user and do necessary stepts to apply changes. - - This should not be used on "change" of user but on first login. - - Args: - url (str): Server url where user authenticated. - token (str): API token used for authentication to server. - username (Union[str, None]): Username related to API token. - """ - - add_server(url, username) - store_token(url, token) - set_environments(url, token) - create_global_connection() diff --git a/common/ayon_common/connection/ui/__init__.py b/common/ayon_common/connection/ui/__init__.py deleted file mode 100644 index 96e573df0d..0000000000 --- a/common/ayon_common/connection/ui/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .login_window import ( - ServerLoginWindow, - ask_to_login, - change_user, -) - - -__all__ = ( - "ServerLoginWindow", - "ask_to_login", - "change_user", -) diff --git a/common/ayon_common/connection/ui/__main__.py b/common/ayon_common/connection/ui/__main__.py deleted file mode 100644 index 719b2b8ef5..0000000000 --- a/common/ayon_common/connection/ui/__main__.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys -import json - -from ayon_common.connection.ui.login_window import ask_to_login - - -def main(output_path): - with open(output_path, "r") as stream: - data = json.load(stream) - - url = data.get("url") - username = data.get("username") - always_on_top = data.get("always_on_top", False) - out_url, out_token, out_username = ask_to_login( - url, username, always_on_top=always_on_top) - - data["output"] = [out_url, out_token, out_username] - with open(output_path, "w") as stream: - json.dump(data, stream) - - -if __name__ == "__main__": - main(sys.argv[-1]) diff --git a/common/ayon_common/connection/ui/login_window.py b/common/ayon_common/connection/ui/login_window.py deleted file mode 100644 index 94c239852e..0000000000 --- a/common/ayon_common/connection/ui/login_window.py +++ /dev/null @@ -1,710 +0,0 @@ -import traceback - -from qtpy import QtWidgets, QtCore, QtGui - -from ayon_api.exceptions import UrlError -from ayon_api.utils import validate_url, login_to_server - -from ayon_common.resources import ( - get_resource_path, - get_icon_path, - load_stylesheet, -) -from ayon_common.ui_utils import set_style_property, get_qt_app - -from .widgets import ( - PressHoverButton, - PlaceholderLineEdit, -) - - -class LogoutConfirmDialog(QtWidgets.QDialog): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.setWindowTitle("Logout confirmation") - - message_widget = QtWidgets.QWidget(self) - - message_label = QtWidgets.QLabel( - ( - "You are going to logout. This action will close this" - " application and will invalidate your login." - " All other applications launched with this login won't be" - " able to use it anymore.

" - "You can cancel logout and only change server and user login" - " in login dialog.

" - "Press OK to confirm logout." - ), - message_widget - ) - message_label.setWordWrap(True) - - message_layout = QtWidgets.QHBoxLayout(message_widget) - message_layout.setContentsMargins(0, 0, 0, 0) - message_layout.addWidget(message_label, 1) - - sep_frame = QtWidgets.QFrame(self) - sep_frame.setObjectName("Separator") - sep_frame.setMinimumHeight(2) - sep_frame.setMaximumHeight(2) - - footer_widget = QtWidgets.QWidget(self) - - cancel_btn = QtWidgets.QPushButton("Cancel", footer_widget) - confirm_btn = QtWidgets.QPushButton("OK", footer_widget) - - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addStretch(1) - footer_layout.addWidget(cancel_btn, 0) - footer_layout.addWidget(confirm_btn, 0) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(message_widget, 0) - main_layout.addStretch(1) - main_layout.addWidget(sep_frame, 0) - main_layout.addWidget(footer_widget, 0) - - cancel_btn.clicked.connect(self._on_cancel_click) - confirm_btn.clicked.connect(self._on_confirm_click) - - self._cancel_btn = cancel_btn - self._confirm_btn = confirm_btn - self._result = False - - def showEvent(self, event): - super().showEvent(event) - self._match_btns_sizes() - - def resizeEvent(self, event): - super().resizeEvent(event) - self._match_btns_sizes() - - def _match_btns_sizes(self): - width = max( - self._cancel_btn.sizeHint().width(), - self._confirm_btn.sizeHint().width() - ) - self._cancel_btn.setMinimumWidth(width) - self._confirm_btn.setMinimumWidth(width) - - def _on_cancel_click(self): - self._result = False - self.reject() - - def _on_confirm_click(self): - self._result = True - self.accept() - - def get_result(self): - return self._result - - -class ServerLoginWindow(QtWidgets.QDialog): - default_width = 410 - default_height = 170 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - icon_path = get_icon_path() - icon = QtGui.QIcon(icon_path) - self.setWindowIcon(icon) - self.setWindowTitle("Login to server") - - edit_icon_path = get_resource_path("edit.png") - edit_icon = QtGui.QIcon(edit_icon_path) - - # --- URL page --- - login_widget = QtWidgets.QWidget(self) - - user_cred_widget = QtWidgets.QWidget(login_widget) - - url_label = QtWidgets.QLabel("URL:", user_cred_widget) - - url_widget = QtWidgets.QWidget(user_cred_widget) - - url_input = PlaceholderLineEdit(url_widget) - url_input.setPlaceholderText("< https://ayon.server.com >") - - url_preview = QtWidgets.QLineEdit(url_widget) - url_preview.setReadOnly(True) - url_preview.setObjectName("LikeDisabledInput") - - url_edit_btn = PressHoverButton(user_cred_widget) - url_edit_btn.setIcon(edit_icon) - url_edit_btn.setObjectName("PasswordBtn") - - url_layout = QtWidgets.QHBoxLayout(url_widget) - url_layout.setContentsMargins(0, 0, 0, 0) - url_layout.addWidget(url_input, 1) - url_layout.addWidget(url_preview, 1) - - # --- URL separator --- - url_cred_sep = QtWidgets.QFrame(self) - url_cred_sep.setObjectName("Separator") - url_cred_sep.setMinimumHeight(2) - url_cred_sep.setMaximumHeight(2) - - # --- Login page --- - username_label = QtWidgets.QLabel("Username:", user_cred_widget) - - username_widget = QtWidgets.QWidget(user_cred_widget) - - username_input = PlaceholderLineEdit(username_widget) - username_input.setPlaceholderText("< Artist >") - - username_preview = QtWidgets.QLineEdit(username_widget) - username_preview.setReadOnly(True) - username_preview.setObjectName("LikeDisabledInput") - - username_edit_btn = PressHoverButton(user_cred_widget) - username_edit_btn.setIcon(edit_icon) - username_edit_btn.setObjectName("PasswordBtn") - - username_layout = QtWidgets.QHBoxLayout(username_widget) - username_layout.setContentsMargins(0, 0, 0, 0) - username_layout.addWidget(username_input, 1) - username_layout.addWidget(username_preview, 1) - - password_label = QtWidgets.QLabel("Password:", user_cred_widget) - password_input = PlaceholderLineEdit(user_cred_widget) - password_input.setPlaceholderText("< *********** >") - password_input.setEchoMode(PlaceholderLineEdit.Password) - - api_label = QtWidgets.QLabel("API key:", user_cred_widget) - api_preview = QtWidgets.QLineEdit(user_cred_widget) - api_preview.setReadOnly(True) - api_preview.setObjectName("LikeDisabledInput") - - show_password_icon_path = get_resource_path("eye.png") - show_password_icon = QtGui.QIcon(show_password_icon_path) - show_password_btn = PressHoverButton(user_cred_widget) - show_password_btn.setObjectName("PasswordBtn") - show_password_btn.setIcon(show_password_icon) - show_password_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - - cred_msg_sep = QtWidgets.QFrame(self) - cred_msg_sep.setObjectName("Separator") - cred_msg_sep.setMinimumHeight(2) - cred_msg_sep.setMaximumHeight(2) - - # --- Credentials inputs --- - user_cred_layout = QtWidgets.QGridLayout(user_cred_widget) - user_cred_layout.setContentsMargins(0, 0, 0, 0) - row = 0 - - user_cred_layout.addWidget(url_label, row, 0, 1, 1) - user_cred_layout.addWidget(url_widget, row, 1, 1, 1) - user_cred_layout.addWidget(url_edit_btn, row, 2, 1, 1) - row += 1 - - user_cred_layout.addWidget(url_cred_sep, row, 0, 1, 3) - row += 1 - - user_cred_layout.addWidget(username_label, row, 0, 1, 1) - user_cred_layout.addWidget(username_widget, row, 1, 1, 1) - user_cred_layout.addWidget(username_edit_btn, row, 2, 2, 1) - row += 1 - - user_cred_layout.addWidget(api_label, row, 0, 1, 1) - user_cred_layout.addWidget(api_preview, row, 1, 1, 1) - row += 1 - - user_cred_layout.addWidget(password_label, row, 0, 1, 1) - user_cred_layout.addWidget(password_input, row, 1, 1, 1) - user_cred_layout.addWidget(show_password_btn, row, 2, 1, 1) - row += 1 - - user_cred_layout.addWidget(cred_msg_sep, row, 0, 1, 3) - row += 1 - - user_cred_layout.setColumnStretch(0, 0) - user_cred_layout.setColumnStretch(1, 1) - user_cred_layout.setColumnStretch(2, 0) - - login_layout = QtWidgets.QVBoxLayout(login_widget) - login_layout.setContentsMargins(0, 0, 0, 0) - login_layout.addWidget(user_cred_widget, 1) - - # --- Messages --- - # Messages for users (e.g. invalid url etc.) - message_label = QtWidgets.QLabel(self) - message_label.setWordWrap(True) - message_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - - footer_widget = QtWidgets.QWidget(self) - logout_btn = QtWidgets.QPushButton("Logout", footer_widget) - user_message = QtWidgets.QLabel(footer_widget) - login_btn = QtWidgets.QPushButton("Login", footer_widget) - confirm_btn = QtWidgets.QPushButton("Confirm", footer_widget) - - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addWidget(logout_btn, 0) - footer_layout.addWidget(user_message, 1) - footer_layout.addWidget(login_btn, 0) - footer_layout.addWidget(confirm_btn, 0) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(login_widget, 0) - main_layout.addWidget(message_label, 0) - main_layout.addStretch(1) - main_layout.addWidget(footer_widget, 0) - - url_input.textChanged.connect(self._on_url_change) - url_input.returnPressed.connect(self._on_url_enter_press) - username_input.textChanged.connect(self._on_user_change) - username_input.returnPressed.connect(self._on_username_enter_press) - password_input.returnPressed.connect(self._on_password_enter_press) - show_password_btn.change_state.connect(self._on_show_password) - url_edit_btn.clicked.connect(self._on_url_edit_click) - username_edit_btn.clicked.connect(self._on_username_edit_click) - logout_btn.clicked.connect(self._on_logout_click) - login_btn.clicked.connect(self._on_login_click) - confirm_btn.clicked.connect(self._on_login_click) - - self._message_label = message_label - - self._url_widget = url_widget - self._url_input = url_input - self._url_preview = url_preview - self._url_edit_btn = url_edit_btn - - self._login_widget = login_widget - - self._user_cred_widget = user_cred_widget - self._username_input = username_input - self._username_preview = username_preview - self._username_edit_btn = username_edit_btn - - self._password_label = password_label - self._password_input = password_input - self._show_password_btn = show_password_btn - self._api_label = api_label - self._api_preview = api_preview - - self._logout_btn = logout_btn - self._user_message = user_message - self._login_btn = login_btn - self._confirm_btn = confirm_btn - - self._url_is_valid = None - self._credentials_are_valid = None - self._result = (None, None, None, False) - self._first_show = True - - self._allow_logout = False - self._logged_in = False - self._url_edit_mode = False - self._username_edit_mode = False - - def set_allow_logout(self, allow_logout): - if allow_logout is self._allow_logout: - return - self._allow_logout = allow_logout - - self._update_states_by_edit_mode() - - def _set_logged_in(self, logged_in): - if logged_in is self._logged_in: - return - self._logged_in = logged_in - - self._update_states_by_edit_mode() - - def _set_url_edit_mode(self, edit_mode): - if self._url_edit_mode is not edit_mode: - self._url_edit_mode = edit_mode - self._update_states_by_edit_mode() - - def _set_username_edit_mode(self, edit_mode): - if self._username_edit_mode is not edit_mode: - self._username_edit_mode = edit_mode - self._update_states_by_edit_mode() - - def _get_url_user_edit(self): - url_edit = True - if self._logged_in and not self._url_edit_mode: - url_edit = False - user_edit = url_edit - if not user_edit and self._logged_in and self._username_edit_mode: - user_edit = True - return url_edit, user_edit - - def _update_states_by_edit_mode(self): - url_edit, user_edit = self._get_url_user_edit() - - self._url_preview.setVisible(not url_edit) - self._url_input.setVisible(url_edit) - self._url_edit_btn.setVisible(self._allow_logout and not url_edit) - - self._username_preview.setVisible(not user_edit) - self._username_input.setVisible(user_edit) - self._username_edit_btn.setVisible( - self._allow_logout and not user_edit - ) - - self._api_preview.setVisible(not user_edit) - self._api_label.setVisible(not user_edit) - - self._password_label.setVisible(user_edit) - self._show_password_btn.setVisible(user_edit) - self._password_input.setVisible(user_edit) - - self._logout_btn.setVisible(self._allow_logout and self._logged_in) - self._login_btn.setVisible(not self._allow_logout) - self._confirm_btn.setVisible(self._allow_logout) - self._update_login_btn_state(url_edit, user_edit) - - def _update_login_btn_state(self, url_edit=None, user_edit=None, url=None): - if url_edit is None: - url_edit, user_edit = self._get_url_user_edit() - - if url is None: - url = self._url_input.text() - - enabled = bool(url) and (url_edit or user_edit) - - self._login_btn.setEnabled(enabled) - self._confirm_btn.setEnabled(enabled) - - def showEvent(self, event): - super().showEvent(event) - if self._first_show: - self._first_show = False - self._on_first_show() - - def _on_first_show(self): - self.setStyleSheet(load_stylesheet()) - self.resize(self.default_width, self.default_height) - self._center_window() - if self._allow_logout is None: - self.set_allow_logout(False) - - self._update_states_by_edit_mode() - if not self._url_input.text(): - widget = self._url_input - elif not self._username_input.text(): - widget = self._username_input - else: - widget = self._password_input - - self._set_input_focus(widget) - - def result(self): - """Result url and token or login. - - Returns: - Union[Tuple[str, str], Tuple[None, None]]: Url and token used for - login if was successful otherwise are both set to None. - """ - return self._result - - def _center_window(self): - """Move window to center of screen.""" - - if hasattr(QtWidgets.QApplication, "desktop"): - desktop = QtWidgets.QApplication.desktop() - screen_idx = desktop.screenNumber(self) - screen_geo = desktop.screenGeometry(screen_idx) - else: - screen = self.screen() - screen_geo = screen.geometry() - - geo = self.frameGeometry() - geo.moveCenter(screen_geo.center()) - if geo.y() < screen_geo.y(): - geo.setY(screen_geo.y()) - self.move(geo.topLeft()) - - def _on_url_change(self, text): - self._update_login_btn_state(url=text) - self._set_url_valid(None) - self._set_credentials_valid(None) - self._url_preview.setText(text) - - def _set_url_valid(self, valid): - if valid is self._url_is_valid: - return - - self._url_is_valid = valid - self._set_input_valid_state(self._url_input, valid) - - def _set_credentials_valid(self, valid): - if self._credentials_are_valid is valid: - return - - self._credentials_are_valid = valid - self._set_input_valid_state(self._username_input, valid) - self._set_input_valid_state(self._password_input, valid) - - def _on_url_enter_press(self): - self._set_input_focus(self._username_input) - - def _on_user_change(self, username): - self._username_preview.setText(username) - - def _on_username_enter_press(self): - self._set_input_focus(self._password_input) - - def _on_password_enter_press(self): - self._login() - - def _on_show_password(self, show_password): - if show_password: - placeholder_text = "< MySecret124 >" - echo_mode = QtWidgets.QLineEdit.Normal - else: - placeholder_text = "< *********** >" - echo_mode = QtWidgets.QLineEdit.Password - - self._password_input.setEchoMode(echo_mode) - self._password_input.setPlaceholderText(placeholder_text) - - def _on_username_edit_click(self): - self._username_edit_mode = True - self._update_states_by_edit_mode() - - def _on_url_edit_click(self): - self._url_edit_mode = True - self._update_states_by_edit_mode() - - def _on_logout_click(self): - dialog = LogoutConfirmDialog(self) - dialog.exec_() - if dialog.get_result(): - self._result = (None, None, None, True) - self.accept() - - def _on_login_click(self): - self._login() - - def _validate_url(self): - """Use url from input to connect and change window state on success. - - Todos: - Threaded check. - """ - - url = self._url_input.text() - valid_url = None - try: - valid_url = validate_url(url) - - except UrlError as exc: - parts = [f"{exc.title}"] - parts.extend(f"- {hint}" for hint in exc.hints) - self._set_message("
".join(parts)) - - except KeyboardInterrupt: - # Reraise KeyboardInterrupt error - raise - - except BaseException: - self._set_unexpected_error() - return - - if valid_url is None: - return False - - self._url_input.setText(valid_url) - return True - - def _login(self): - if ( - not self._login_btn.isEnabled() - and not self._confirm_btn.isEnabled() - ): - return - - if not self._url_is_valid: - self._set_url_valid(self._validate_url()) - - if not self._url_is_valid: - self._set_input_focus(self._url_input) - self._set_credentials_valid(None) - return - - self._clear_message() - - url = self._url_input.text() - username = self._username_input.text() - password = self._password_input.text() - try: - token = login_to_server(url, username, password) - except BaseException: - self._set_unexpected_error() - return - - if token is not None: - self._result = (url, token, username, False) - self.accept() - return - - self._set_credentials_valid(False) - message_lines = ["Invalid credentials"] - if not username.strip(): - message_lines.append("- Username is not filled") - - if not password.strip(): - message_lines.append("- Password is not filled") - - if username and password: - message_lines.append("- Check your credentials") - - self._set_message("
".join(message_lines)) - self._set_input_focus(self._username_input) - - def _set_input_focus(self, widget): - widget.setFocus(QtCore.Qt.MouseFocusReason) - - def _set_input_valid_state(self, widget, valid): - state = "" - if valid is True: - state = "valid" - elif valid is False: - state = "invalid" - set_style_property(widget, "state", state) - - def _set_message(self, message): - self._message_label.setText(message) - - def _clear_message(self): - self._message_label.setText("") - - def _set_unexpected_error(self): - # TODO add traceback somewhere - # - maybe a button to show or copy? - traceback.print_exc() - lines = [ - "Unexpected error happened", - "- Can be caused by wrong url (leading elsewhere)" - ] - self._set_message("
".join(lines)) - - def set_url(self, url): - self._url_preview.setText(url) - self._url_input.setText(url) - self._validate_url() - - def set_username(self, username): - self._username_preview.setText(username) - self._username_input.setText(username) - - def _set_api_key(self, api_key): - if not api_key or len(api_key) < 3: - self._api_preview.setText(api_key or "") - return - - api_key_len = len(api_key) - offset = 6 - if api_key_len < offset: - offset = api_key_len // 2 - api_key = api_key[:offset] + "." * (api_key_len - offset) - - self._api_preview.setText(api_key) - - def set_logged_in( - self, - logged_in, - url=None, - username=None, - api_key=None, - allow_logout=None - ): - if url is not None: - self.set_url(url) - - if username is not None: - self.set_username(username) - - if api_key: - self._set_api_key(api_key) - - if logged_in and allow_logout is None: - allow_logout = True - - self._set_logged_in(logged_in) - - if allow_logout: - self.set_allow_logout(True) - elif allow_logout is False: - self.set_allow_logout(False) - - -def ask_to_login(url=None, username=None, always_on_top=False): - """Ask user to login using Qt dialog. - - Function creates new QApplication if is not created yet. - - Args: - url (Optional[str]): Server url that will be prefilled in dialog. - username (Optional[str]): Username that will be prefilled in dialog. - always_on_top (Optional[bool]): Window will be drawn on top of - other windows. - - Returns: - tuple[str, str, str]: Returns Url, user's token and username. Url can - be changed during dialog lifetime that's why the url is returned. - """ - - app_instance = get_qt_app() - - window = ServerLoginWindow() - if always_on_top: - window.setWindowFlags( - window.windowFlags() - | QtCore.Qt.WindowStaysOnTopHint - ) - - if url: - window.set_url(url) - - if username: - window.set_username(username) - - if not app_instance.startingUp(): - window.exec_() - else: - window.open() - app_instance.exec_() - result = window.result() - out_url, out_token, out_username, _ = result - return out_url, out_token, out_username - - -def change_user(url, username, api_key, always_on_top=False): - """Ask user to login using Qt dialog. - - Function creates new QApplication if is not created yet. - - Args: - url (str): Server url that will be prefilled in dialog. - username (str): Username that will be prefilled in dialog. - api_key (str): API key that will be prefilled in dialog. - always_on_top (Optional[bool]): Window will be drawn on top of - other windows. - - Returns: - Tuple[str, str]: Returns Url and user's token. Url can be changed - during dialog lifetime that's why the url is returned. - """ - - app_instance = get_qt_app() - window = ServerLoginWindow() - if always_on_top: - window.setWindowFlags( - window.windowFlags() - | QtCore.Qt.WindowStaysOnTopHint - ) - window.set_logged_in(True, url, username, api_key) - - if not app_instance.startingUp(): - window.exec_() - else: - window.open() - # This can become main Qt loop. Maybe should live elsewhere - app_instance.exec_() - return window.result() diff --git a/common/ayon_common/connection/ui/widgets.py b/common/ayon_common/connection/ui/widgets.py deleted file mode 100644 index 78b73e056d..0000000000 --- a/common/ayon_common/connection/ui/widgets.py +++ /dev/null @@ -1,47 +0,0 @@ -from qtpy import QtWidgets, QtCore, QtGui - - -class PressHoverButton(QtWidgets.QPushButton): - """Keep track about mouse press/release and enter/leave.""" - - _mouse_pressed = False - _mouse_hovered = False - change_state = QtCore.Signal(bool) - - def mousePressEvent(self, event): - self._mouse_pressed = True - self._mouse_hovered = True - self.change_state.emit(self._mouse_hovered) - super(PressHoverButton, self).mousePressEvent(event) - - def mouseReleaseEvent(self, event): - self._mouse_pressed = False - self._mouse_hovered = False - self.change_state.emit(self._mouse_hovered) - super(PressHoverButton, self).mouseReleaseEvent(event) - - def mouseMoveEvent(self, event): - mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos()) - under_mouse = self.rect().contains(mouse_pos) - if under_mouse != self._mouse_hovered: - self._mouse_hovered = under_mouse - self.change_state.emit(self._mouse_hovered) - - super(PressHoverButton, self).mouseMoveEvent(event) - - -class PlaceholderLineEdit(QtWidgets.QLineEdit): - """Set placeholder color of QLineEdit in Qt 5.12 and higher.""" - - def __init__(self, *args, **kwargs): - super(PlaceholderLineEdit, self).__init__(*args, **kwargs) - # Change placeholder palette color - if hasattr(QtGui.QPalette, "PlaceholderText"): - filter_palette = self.palette() - color = QtGui.QColor("#D3D8DE") - color.setAlpha(67) - filter_palette.setColor( - QtGui.QPalette.PlaceholderText, - color - ) - self.setPalette(filter_palette) diff --git a/common/ayon_common/distribution/README.md b/common/ayon_common/distribution/README.md deleted file mode 100644 index f1c34ba722..0000000000 --- a/common/ayon_common/distribution/README.md +++ /dev/null @@ -1,18 +0,0 @@ -Addon distribution tool ------------------------- - -Code in this folder is backend portion of Addon distribution logic for v4 server. - -Each host, module will be separate Addon in the future. Each v4 server could run different set of Addons. - -Client (running on artist machine) will in the first step ask v4 for list of enabled addons. -(It expects list of json documents matching to `addon_distribution.py:AddonInfo` object.) -Next it will compare presence of enabled addon version in local folder. In the case of missing version of -an addon, client will use information in the addon to download (from http/shared local disk/git) zip file -and unzip it. - -Required part of addon distribution will be sharing of dependencies (python libraries, utilities) which is not part of this folder. - -Location of this folder might change in the future as it will be required for a clint to add this folder to sys.path reliably. - -This code needs to be independent on Openpype code as much as possible! diff --git a/common/ayon_common/distribution/__init__.py b/common/ayon_common/distribution/__init__.py deleted file mode 100644 index e3c0f0e161..0000000000 --- a/common/ayon_common/distribution/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .control import AyonDistribution, BundleNotFoundError -from .utils import show_missing_bundle_information - - -__all__ = ( - "AyonDistribution", - "BundleNotFoundError", - "show_missing_bundle_information", -) diff --git a/common/ayon_common/distribution/control.py b/common/ayon_common/distribution/control.py deleted file mode 100644 index 95c221d753..0000000000 --- a/common/ayon_common/distribution/control.py +++ /dev/null @@ -1,1116 +0,0 @@ -import os -import sys -import json -import traceback -import collections -import datetime -import logging -import shutil -import threading -import platform -import attr -from enum import Enum - -import ayon_api - -from ayon_common.utils import is_staging_enabled - -from .utils import ( - get_addons_dir, - get_dependencies_dir, -) -from .downloaders import get_default_download_factory -from .data_structures import ( - AddonInfo, - DependencyItem, - Bundle, -) - -NOT_SET = type("UNKNOWN", (), {"__bool__": lambda: False})() - - -class BundleNotFoundError(Exception): - """Bundle name is defined but is not available on server. - - Args: - bundle_name (str): Name of bundle that was not found. - """ - - def __init__(self, bundle_name): - self.bundle_name = bundle_name - super().__init__( - f"Bundle '{bundle_name}' is not available on server" - ) - - -class UpdateState(Enum): - UNKNOWN = "unknown" - UPDATED = "udated" - OUTDATED = "outdated" - UPDATE_FAILED = "failed" - MISS_SOURCE_FILES = "miss_source_files" - - -class DistributeTransferProgress: - """Progress of single source item in 'DistributionItem'. - - The item is to keep track of single source item. - """ - - def __init__(self): - self._transfer_progress = ayon_api.TransferProgress() - self._started = False - self._failed = False - self._fail_reason = None - self._unzip_started = False - self._unzip_finished = False - self._hash_check_started = False - self._hash_check_finished = False - - def set_started(self): - """Call when source distribution starts.""" - - self._started = True - - def set_failed(self, reason): - """Set source distribution as failed. - - Args: - reason (str): Error message why the transfer failed. - """ - - self._failed = True - self._fail_reason = reason - - def set_hash_check_started(self): - """Call just before hash check starts.""" - - self._hash_check_started = True - - def set_hash_check_finished(self): - """Call just after hash check finishes.""" - - self._hash_check_finished = True - - def set_unzip_started(self): - """Call just before unzip starts.""" - - self._unzip_started = True - - def set_unzip_finished(self): - """Call just after unzip finishes.""" - - self._unzip_finished = True - - @property - def is_running(self): - """Source distribution is in progress. - - Returns: - bool: Transfer is in progress. - """ - - return bool( - self._started - and not self._failed - and not self._hash_check_finished - ) - - @property - def transfer_progress(self): - """Source file 'download' progress tracker. - - Returns: - ayon_api.TransferProgress.: Content download progress. - """ - - return self._transfer_progress - - @property - def started(self): - return self._started - - @property - def hash_check_started(self): - return self._hash_check_started - - @property - def hash_check_finished(self): - return self._has_check_finished - - @property - def unzip_started(self): - return self._unzip_started - - @property - def unzip_finished(self): - return self._unzip_finished - - @property - def failed(self): - return self._failed or self._transfer_progress.failed - - @property - def fail_reason(self): - return self._fail_reason or self._transfer_progress.fail_reason - - -class DistributionItem: - """Distribution item with sources and target directories. - - Distribution item can be an addon or dependency package. Distribution item - can be already distributed and don't need any progression. The item keeps - track of the progress. The reason is to be able to use the distribution - items as source data for UI without implementing the same logic. - - Distribution is "state" based. Distribution can be 'UPDATED' or 'OUTDATED' - at the initialization. If item is 'UPDATED' the distribution is skipped - and 'OUTDATED' will trigger the distribution process. - - Because the distribution may have multiple sources each source has own - progress item. - - Args: - state (UpdateState): Initial state (UpdateState.UPDATED or - UpdateState.OUTDATED). - unzip_dirpath (str): Path to directory where zip is downloaded. - download_dirpath (str): Path to directory where file is unzipped. - file_hash (str): Hash of file for validation. - factory (DownloadFactory): Downloaders factory object. - sources (List[SourceInfo]): Possible sources to receive the - distribution item. - downloader_data (Dict[str, Any]): More information for downloaders. - item_label (str): Label used in log outputs (and in UI). - logger (logging.Logger): Logger object. - """ - - def __init__( - self, - state, - unzip_dirpath, - download_dirpath, - file_hash, - factory, - sources, - downloader_data, - item_label, - logger=None, - ): - if logger is None: - logger = logging.getLogger(self.__class__.__name__) - self.log = logger - self.state = state - self.unzip_dirpath = unzip_dirpath - self.download_dirpath = download_dirpath - self.file_hash = file_hash - self.factory = factory - self.sources = [ - (source, DistributeTransferProgress()) - for source in sources - ] - self.downloader_data = downloader_data - self.item_label = item_label - - self._need_distribution = state != UpdateState.UPDATED - self._current_source_progress = None - self._used_source_progress = None - self._used_source = None - self._dist_started = False - self._dist_finished = False - - self._error_msg = None - self._error_detail = None - - @property - def need_distribution(self): - """Need distribution based on initial state. - - Returns: - bool: Need distribution. - """ - - return self._need_distribution - - @property - def current_source_progress(self): - """Currently processed source progress object. - - Returns: - Union[DistributeTransferProgress, None]: Transfer progress or None. - """ - - return self._current_source_progress - - @property - def used_source_progress(self): - """Transfer progress that successfully distributed the item. - - Returns: - Union[DistributeTransferProgress, None]: Transfer progress or None. - """ - - return self._used_source_progress - - @property - def used_source(self): - """Data of source item. - - Returns: - Union[Dict[str, Any], None]: SourceInfo data or None. - """ - - return self._used_source - - @property - def error_message(self): - """Reason why distribution item failed. - - Returns: - Union[str, None]: Error message. - """ - - return self._error_msg - - @property - def error_detail(self): - """Detailed reason why distribution item failed. - - Returns: - Union[str, None]: Detailed information (maybe traceback). - """ - - return self._error_detail - - def _distribute(self): - if not self.sources: - message = ( - f"{self.item_label}: Don't have" - " any sources to download from." - ) - self.log.error(message) - self._error_msg = message - self.state = UpdateState.MISS_SOURCE_FILES - return - - download_dirpath = self.download_dirpath - unzip_dirpath = self.unzip_dirpath - for source, source_progress in self.sources: - self._current_source_progress = source_progress - source_progress.set_started() - - # Remove directory if exists - if os.path.isdir(unzip_dirpath): - self.log.debug(f"Cleaning {unzip_dirpath}") - shutil.rmtree(unzip_dirpath) - - # Create directory - os.makedirs(unzip_dirpath) - if not os.path.isdir(download_dirpath): - os.makedirs(download_dirpath) - - try: - downloader = self.factory.get_downloader(source.type) - except Exception: - message = f"Unknown downloader {source.type}" - source_progress.set_failed(message) - self.log.warning(message, exc_info=True) - continue - - source_data = attr.asdict(source) - cleanup_args = ( - source_data, - download_dirpath, - self.downloader_data - ) - - try: - zip_filepath = downloader.download( - source_data, - download_dirpath, - self.downloader_data, - source_progress.transfer_progress, - ) - except Exception: - message = "Failed to download source" - source_progress.set_failed(message) - self.log.warning( - f"{self.item_label}: {message}", - exc_info=True - ) - downloader.cleanup(*cleanup_args) - continue - - source_progress.set_hash_check_started() - try: - downloader.check_hash(zip_filepath, self.file_hash) - except Exception: - message = "File hash does not match" - source_progress.set_failed(message) - self.log.warning( - f"{self.item_label}: {message}", - exc_info=True - ) - downloader.cleanup(*cleanup_args) - continue - - source_progress.set_hash_check_finished() - source_progress.set_unzip_started() - try: - downloader.unzip(zip_filepath, unzip_dirpath) - except Exception: - message = "Couldn't unzip source file" - source_progress.set_failed(message) - self.log.warning( - f"{self.item_label}: {message}", - exc_info=True - ) - downloader.cleanup(*cleanup_args) - continue - - source_progress.set_unzip_finished() - downloader.cleanup(*cleanup_args) - self.state = UpdateState.UPDATED - self._used_source = source_data - break - - last_progress = self._current_source_progress - self._current_source_progress = None - if self.state == UpdateState.UPDATED: - self._used_source_progress = last_progress - self.log.info(f"{self.item_label}: Distributed") - return - - self.log.error(f"{self.item_label}: Failed to distribute") - self._error_msg = "Failed to receive or install source files" - - def distribute(self): - """Execute distribution logic.""" - - if not self.need_distribution or self._dist_started: - return - - self._dist_started = True - try: - if self.state == UpdateState.OUTDATED: - self._distribute() - - except Exception as exc: - self.state = UpdateState.UPDATE_FAILED - self._error_msg = str(exc) - self._error_detail = "".join( - traceback.format_exception(*sys.exc_info()) - ) - self.log.error( - f"{self.item_label}: Distibution filed", - exc_info=True - ) - - finally: - self._dist_finished = True - if self.state == UpdateState.OUTDATED: - self.state = UpdateState.UPDATE_FAILED - self._error_msg = "Distribution failed" - - if ( - self.state != UpdateState.UPDATED - and self.unzip_dirpath - and os.path.isdir(self.unzip_dirpath) - ): - self.log.debug(f"Cleaning {self.unzip_dirpath}") - shutil.rmtree(self.unzip_dirpath) - - -class AyonDistribution: - """Distribution control. - - Receive information from server what addons and dependency packages - should be available locally and prepare/validate their distribution. - - Arguments are available for testing of the class. - - Args: - addon_dirpath (Optional[str]): Where addons will be stored. - dependency_dirpath (Optional[str]): Where dependencies will be stored. - dist_factory (Optional[DownloadFactory]): Factory which cares about - downloading of items based on source type. - addons_info (Optional[list[dict[str, Any]]): List of prepared - addons' info. - dependency_packages_info (Optional[list[dict[str, Any]]): Info - about packages from server. - bundles_info (Optional[Dict[str, Any]]): Info about - bundles. - bundle_name (Optional[str]): Name of bundle to use. If not passed - an environment variable 'AYON_BUNDLE_NAME' is checked for value. - When both are not available the bundle is defined by 'use_staging' - value. - use_staging (Optional[bool]): Use staging versions of an addon. - If not passed, 'is_staging_enabled' is used as default value. - """ - - def __init__( - self, - addon_dirpath=None, - dependency_dirpath=None, - dist_factory=None, - addons_info=NOT_SET, - dependency_packages_info=NOT_SET, - bundles_info=NOT_SET, - bundle_name=NOT_SET, - use_staging=None - ): - self._log = None - - self._dist_started = False - self._dist_finished = False - - self._addons_dirpath = addon_dirpath or get_addons_dir() - self._dependency_dirpath = dependency_dirpath or get_dependencies_dir() - self._dist_factory = ( - dist_factory or get_default_download_factory() - ) - - if bundle_name is NOT_SET: - bundle_name = os.environ.get("AYON_BUNDLE_NAME", NOT_SET) - - # Raw addons data from server - self._addons_info = addons_info - # Prepared data as Addon objects - self._addon_items = NOT_SET - # Distrubtion items of addons - # - only those addons and versions that should be distributed - self._addon_dist_items = NOT_SET - - # Raw dependency packages data from server - self._dependency_packages_info = dependency_packages_info - # Prepared dependency packages as objects - self._dependency_packages_items = NOT_SET - # Dependency package item that should be used - self._dependency_package_item = NOT_SET - # Distribution item of dependency package - self._dependency_dist_item = NOT_SET - - # Raw bundles data from server - self._bundles_info = bundles_info - # Bundles as objects - self._bundle_items = NOT_SET - - # Bundle that should be used in production - self._production_bundle = NOT_SET - # Bundle that should be used in staging - self._staging_bundle = NOT_SET - # Boolean that defines if staging bundle should be used - self._use_staging = use_staging - - # Specific bundle name should be used - self._bundle_name = bundle_name - # Final bundle that will be used - self._bundle = NOT_SET - - @property - def use_staging(self): - """Staging version of a bundle should be used. - - This value is completely ignored if specific bundle name should - be used. - - Returns: - bool: True if staging version should be used. - """ - - if self._use_staging is None: - self._use_staging = is_staging_enabled() - return self._use_staging - - @property - def log(self): - """Helper to access logger. - - Returns: - logging.Logger: Logger instance. - """ - if self._log is None: - self._log = logging.getLogger(self.__class__.__name__) - return self._log - - @property - def bundles_info(self): - """ - - Returns: - dict[str, dict[str, Any]]: Bundles information from server. - """ - - if self._bundles_info is NOT_SET: - self._bundles_info = ayon_api.get_bundles() - return self._bundles_info - - @property - def bundle_items(self): - """ - - Returns: - list[Bundle]: List of bundles info. - """ - - if self._bundle_items is NOT_SET: - self._bundle_items = [ - Bundle.from_dict(info) - for info in self.bundles_info["bundles"] - ] - return self._bundle_items - - def _prepare_production_staging_bundles(self): - production_bundle = None - staging_bundle = None - for bundle in self.bundle_items: - if bundle.is_production: - production_bundle = bundle - if bundle.is_staging: - staging_bundle = bundle - self._production_bundle = production_bundle - self._staging_bundle = staging_bundle - - @property - def production_bundle(self): - """ - Returns: - Union[Bundle, None]: Bundle that should be used in production. - """ - - if self._production_bundle is NOT_SET: - self._prepare_production_staging_bundles() - return self._production_bundle - - @property - def staging_bundle(self): - """ - Returns: - Union[Bundle, None]: Bundle that should be used in staging. - """ - - if self._staging_bundle is NOT_SET: - self._prepare_production_staging_bundles() - return self._staging_bundle - - @property - def bundle_to_use(self): - """Bundle that will be used for distribution. - - Bundle that should be used can be affected by 'bundle_name' - or 'use_staging'. - - Returns: - Union[Bundle, None]: Bundle that will be used for distribution - or None. - - Raises: - BundleNotFoundError: When bundle name to use is defined - but is not available on server. - """ - - if self._bundle is NOT_SET: - if self._bundle_name is not NOT_SET: - bundle = next( - ( - bundle - for bundle in self.bundle_items - if bundle.name == self._bundle_name - ), - None - ) - if bundle is None: - raise BundleNotFoundError(self._bundle_name) - - self._bundle = bundle - elif self.use_staging: - self._bundle = self.staging_bundle - else: - self._bundle = self.production_bundle - return self._bundle - - @property - def bundle_name_to_use(self): - bundle = self.bundle_to_use - return None if bundle is None else bundle.name - - @property - def addons_info(self): - """Server information about available addons. - - Returns: - Dict[str, dict[str, Any]: Addon info by addon name. - """ - - if self._addons_info is NOT_SET: - server_info = ayon_api.get_addons_info(details=True) - self._addons_info = server_info["addons"] - return self._addons_info - - @property - def addon_items(self): - """Information about available addons on server. - - Addons may require distribution of files. For those addons will be - created 'DistributionItem' handling distribution itself. - - Returns: - Dict[str, AddonInfo]: Addon info object by addon name. - """ - - if self._addon_items is NOT_SET: - addons_info = {} - for addon in self.addons_info: - addon_info = AddonInfo.from_dict(addon) - addons_info[addon_info.name] = addon_info - self._addon_items = addons_info - return self._addon_items - - @property - def dependency_packages_info(self): - """Server information about available dependency packages. - - Notes: - For testing purposes it is possible to pass dependency packages - information to '__init__'. - - Returns: - list[dict[str, Any]]: Dependency packages information. - """ - - if self._dependency_packages_info is NOT_SET: - self._dependency_packages_info = ( - ayon_api.get_dependency_packages())["packages"] - return self._dependency_packages_info - - @property - def dependency_packages_items(self): - """Dependency packages as objects. - - Returns: - dict[str, DependencyItem]: Dependency packages as objects by name. - """ - - if self._dependency_packages_items is NOT_SET: - dependenc_package_items = {} - for item in self.dependency_packages_info: - item = DependencyItem.from_dict(item) - dependenc_package_items[item.name] = item - self._dependency_packages_items = dependenc_package_items - return self._dependency_packages_items - - @property - def dependency_package_item(self): - """Dependency package item that should be used by bundle. - - Returns: - Union[None, Dict[str, Any]]: None if bundle does not have - specified dependency package. - """ - - if self._dependency_package_item is NOT_SET: - dependency_package_item = None - bundle = self.bundle_to_use - if bundle is not None: - package_name = bundle.dependency_packages.get( - platform.system().lower() - ) - dependency_package_item = self.dependency_packages_items.get( - package_name) - self._dependency_package_item = dependency_package_item - return self._dependency_package_item - - def _prepare_current_addon_dist_items(self): - addons_metadata = self.get_addons_metadata() - output = [] - addon_versions = {} - bundle = self.bundle_to_use - if bundle is not None: - addon_versions = bundle.addon_versions - for addon_name, addon_item in self.addon_items.items(): - addon_version = addon_versions.get(addon_name) - # Addon is not in bundle -> Skip - if addon_version is None: - continue - - addon_version_item = addon_item.versions.get(addon_version) - # Addon version is not available in addons info - # - TODO handle this case (raise error, skip, store, report, ...) - if addon_version_item is None: - print( - f"Version '{addon_version}' of addon '{addon_name}'" - " is not available on server." - ) - continue - - if not addon_version_item.require_distribution: - continue - full_name = addon_version_item.full_name - addon_dest = os.path.join(self._addons_dirpath, full_name) - self.log.debug(f"Checking {full_name} in {addon_dest}") - addon_in_metadata = ( - addon_name in addons_metadata - and addon_version_item.version in addons_metadata[addon_name] - ) - if addon_in_metadata and os.path.isdir(addon_dest): - self.log.debug( - f"Addon version folder {addon_dest} already exists." - ) - state = UpdateState.UPDATED - - else: - state = UpdateState.OUTDATED - - downloader_data = { - "type": "addon", - "name": addon_name, - "version": addon_version - } - - dist_item = DistributionItem( - state, - addon_dest, - addon_dest, - addon_version_item.hash, - self._dist_factory, - list(addon_version_item.sources), - downloader_data, - full_name, - self.log - ) - output.append({ - "dist_item": dist_item, - "addon_name": addon_name, - "addon_version": addon_version, - "addon_item": addon_item, - "addon_version_item": addon_version_item, - }) - return output - - def _prepare_dependency_progress(self): - package = self.dependency_package_item - if package is None: - return None - - metadata = self.get_dependency_metadata() - downloader_data = { - "type": "dependency_package", - "name": package.name, - "platform": package.platform_name - } - zip_dir = package_dir = os.path.join( - self._dependency_dirpath, package.name - ) - self.log.debug(f"Checking {package.name} in {package_dir}") - - if not os.path.isdir(package_dir) or package.name not in metadata: - state = UpdateState.OUTDATED - else: - state = UpdateState.UPDATED - - return DistributionItem( - state, - zip_dir, - package_dir, - package.checksum, - self._dist_factory, - package.sources, - downloader_data, - package.name, - self.log, - ) - - def get_addon_dist_items(self): - """Addon distribution items. - - These items describe source files required by addon to be available on - machine. Each item may have 0-n source information from where can be - obtained. If file is already available it's state will be 'UPDATED'. - - Example output: - [ - { - "dist_item": DistributionItem, - "addon_name": str, - "addon_version": str, - "addon_item": AddonInfo, - "addon_version_item": AddonVersionInfo - }, { - ... - } - ] - - Returns: - list[dict[str, Any]]: Distribution items with addon version item. - """ - - if self._addon_dist_items is NOT_SET: - self._addon_dist_items = ( - self._prepare_current_addon_dist_items()) - return self._addon_dist_items - - def get_dependency_dist_item(self): - """Dependency package distribution item. - - Item describe source files required by server to be available on - machine. Item may have 0-n source information from where can be - obtained. If file is already available it's state will be 'UPDATED'. - - 'None' is returned if server does not have defined any dependency - package. - - Returns: - Union[None, DistributionItem]: Dependency item or None if server - does not have specified any dependency package. - """ - - if self._dependency_dist_item is NOT_SET: - self._dependency_dist_item = self._prepare_dependency_progress() - return self._dependency_dist_item - - def get_dependency_metadata_filepath(self): - """Path to distribution metadata file. - - Metadata contain information about distributed packages, used source, - expected file hash and time when file was distributed. - - Returns: - str: Path to a file where dependency package metadata are stored. - """ - - return os.path.join(self._dependency_dirpath, "dependency.json") - - def get_addons_metadata_filepath(self): - """Path to addons metadata file. - - Metadata contain information about distributed addons, used sources, - expected file hashes and time when files were distributed. - - Returns: - str: Path to a file where addons metadata are stored. - """ - - return os.path.join(self._addons_dirpath, "addons.json") - - def read_metadata_file(self, filepath, default_value=None): - """Read json file from path. - - Method creates the file when does not exist with default value. - - Args: - filepath (str): Path to json file. - default_value (Union[Dict[str, Any], List[Any], None]): Default - value if the file is not available (or valid). - - Returns: - Union[Dict[str, Any], List[Any]]: Value from file. - """ - - if default_value is None: - default_value = {} - - if not os.path.exists(filepath): - return default_value - - try: - with open(filepath, "r") as stream: - data = json.load(stream) - except ValueError: - data = default_value - return data - - def save_metadata_file(self, filepath, data): - """Store data to json file. - - Method creates the file when does not exist. - - Args: - filepath (str): Path to json file. - data (Union[Dict[str, Any], List[Any]]): Data to store into file. - """ - - if not os.path.exists(filepath): - dirpath = os.path.dirname(filepath) - if not os.path.exists(dirpath): - os.makedirs(dirpath) - with open(filepath, "w") as stream: - json.dump(data, stream, indent=4) - - def get_dependency_metadata(self): - filepath = self.get_dependency_metadata_filepath() - return self.read_metadata_file(filepath, {}) - - def update_dependency_metadata(self, package_name, data): - dependency_metadata = self.get_dependency_metadata() - dependency_metadata[package_name] = data - filepath = self.get_dependency_metadata_filepath() - self.save_metadata_file(filepath, dependency_metadata) - - def get_addons_metadata(self): - filepath = self.get_addons_metadata_filepath() - return self.read_metadata_file(filepath, {}) - - def update_addons_metadata(self, addons_information): - if not addons_information: - return - addons_metadata = self.get_addons_metadata() - for addon_name, version_value in addons_information.items(): - if addon_name not in addons_metadata: - addons_metadata[addon_name] = {} - for addon_version, version_data in version_value.items(): - addons_metadata[addon_name][addon_version] = version_data - - filepath = self.get_addons_metadata_filepath() - self.save_metadata_file(filepath, addons_metadata) - - def finish_distribution(self): - """Store metadata about distributed items.""" - - self._dist_finished = True - stored_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - dependency_dist_item = self.get_dependency_dist_item() - if ( - dependency_dist_item is not None - and dependency_dist_item.need_distribution - and dependency_dist_item.state == UpdateState.UPDATED - ): - package = self.dependency_package - source = dependency_dist_item.used_source - if source is not None: - data = { - "source": source, - "file_hash": dependency_dist_item.file_hash, - "distributed_dt": stored_time - } - self.update_dependency_metadata(package.name, data) - - addons_info = {} - for item in self.get_addon_dist_items(): - dist_item = item["dist_item"] - if ( - not dist_item.need_distribution - or dist_item.state != UpdateState.UPDATED - ): - continue - - source_data = dist_item.used_source - if not source_data: - continue - - addon_name = item["addon_name"] - addon_version = item["addon_version"] - addons_info.setdefault(addon_name, {}) - addons_info[addon_name][addon_version] = { - "source": source_data, - "file_hash": dist_item.file_hash, - "distributed_dt": stored_time - } - - self.update_addons_metadata(addons_info) - - def get_all_distribution_items(self): - """Distribution items required by server. - - Items contain dependency package item and all addons that are enabled - and have distribution requirements. - - Items can be already available on machine. - - Returns: - List[DistributionItem]: Distribution items required by server. - """ - - output = [ - item["dist_item"] - for item in self.get_addon_dist_items() - ] - dependency_dist_item = self.get_dependency_dist_item() - if dependency_dist_item is not None: - output.insert(0, dependency_dist_item) - - return output - - def distribute(self, threaded=False): - """Distribute all missing items. - - Method will try to distribute all items that are required by server. - - This method does not handle failed items. To validate the result call - 'validate_distribution' when this method finishes. - - Args: - threaded (bool): Distribute items in threads. - """ - - if self._dist_started: - raise RuntimeError("Distribution already started") - self._dist_started = True - threads = collections.deque() - for item in self.get_all_distribution_items(): - if threaded: - threads.append(threading.Thread(target=item.distribute)) - else: - item.distribute() - - while threads: - thread = threads.popleft() - if thread.is_alive(): - threads.append(thread) - else: - thread.join() - - self.finish_distribution() - - def validate_distribution(self): - """Check if all required distribution items are distributed. - - Raises: - RuntimeError: Any of items is not available. - """ - - invalid = [] - dependency_package = self.get_dependency_dist_item() - if ( - dependency_package is not None - and dependency_package.state != UpdateState.UPDATED - ): - invalid.append("Dependency package") - - for item in self.get_addon_dist_items(): - dist_item = item["dist_item"] - if dist_item.state != UpdateState.UPDATED: - invalid.append(item["addon_name"]) - - if not invalid: - return - - raise RuntimeError("Failed to distribute {}".format( - ", ".join([f'"{item}"' for item in invalid]) - )) - - def get_sys_paths(self): - """Get all paths to python packages that should be added to python. - - These paths lead to addon directories and python dependencies in - dependency package. - - Todos: - Add dependency package directory to output. ATM is not structure of - dependency package 100% defined. - - Returns: - List[str]: Paths that should be added to 'sys.path' and - 'PYTHONPATH'. - """ - - output = [] - for item in self.get_all_distribution_items(): - if item.state != UpdateState.UPDATED: - continue - unzip_dirpath = item.unzip_dirpath - if unzip_dirpath and os.path.exists(unzip_dirpath): - output.append(unzip_dirpath) - return output - - -def cli(*args): - raise NotImplementedError diff --git a/common/ayon_common/distribution/data_structures.py b/common/ayon_common/distribution/data_structures.py deleted file mode 100644 index aa93d4ed71..0000000000 --- a/common/ayon_common/distribution/data_structures.py +++ /dev/null @@ -1,265 +0,0 @@ -import attr -from enum import Enum - - -class UrlType(Enum): - HTTP = "http" - GIT = "git" - FILESYSTEM = "filesystem" - SERVER = "server" - - -@attr.s -class MultiPlatformValue(object): - windows = attr.ib(default=None) - linux = attr.ib(default=None) - darwin = attr.ib(default=None) - - -@attr.s -class SourceInfo(object): - type = attr.ib() - - -@attr.s -class LocalSourceInfo(SourceInfo): - path = attr.ib(default=attr.Factory(MultiPlatformValue)) - - -@attr.s -class WebSourceInfo(SourceInfo): - url = attr.ib(default=None) - headers = attr.ib(default=None) - filename = attr.ib(default=None) - - -@attr.s -class ServerSourceInfo(SourceInfo): - filename = attr.ib(default=None) - path = attr.ib(default=None) - - -def convert_source(source): - """Create source object from data information. - - Args: - source (Dict[str, any]): Information about source. - - Returns: - Union[None, SourceInfo]: Object with source information if type is - known. - """ - - source_type = source.get("type") - if not source_type: - return None - - if source_type == UrlType.FILESYSTEM.value: - return LocalSourceInfo( - type=source_type, - path=source["path"] - ) - - if source_type == UrlType.HTTP.value: - url = source["path"] - return WebSourceInfo( - type=source_type, - url=url, - headers=source.get("headers"), - filename=source.get("filename") - ) - - if source_type == UrlType.SERVER.value: - return ServerSourceInfo( - type=source_type, - filename=source.get("filename"), - path=source.get("path") - ) - - -def prepare_sources(src_sources): - sources = [] - unknown_sources = [] - for source in (src_sources or []): - dependency_source = convert_source(source) - if dependency_source is not None: - sources.append(dependency_source) - else: - print(f"Unknown source {source.get('type')}") - unknown_sources.append(source) - return sources, unknown_sources - - -@attr.s -class VersionData(object): - version_data = attr.ib(default=None) - - -@attr.s -class AddonVersionInfo(object): - version = attr.ib() - full_name = attr.ib() - title = attr.ib(default=None) - require_distribution = attr.ib(default=False) - sources = attr.ib(default=attr.Factory(list)) - unknown_sources = attr.ib(default=attr.Factory(list)) - hash = attr.ib(default=None) - - @classmethod - def from_dict( - cls, addon_name, addon_title, addon_version, version_data - ): - """Addon version info. - - Args: - addon_name (str): Name of addon. - addon_title (str): Title of addon. - addon_version (str): Version of addon. - version_data (dict[str, Any]): Addon version information from - server. - - Returns: - AddonVersionInfo: Addon version info. - """ - - full_name = f"{addon_name}_{addon_version}" - title = f"{addon_title} {addon_version}" - - source_info = version_data.get("clientSourceInfo") - require_distribution = source_info is not None - sources, unknown_sources = prepare_sources(source_info) - - return cls( - version=addon_version, - full_name=full_name, - require_distribution=require_distribution, - sources=sources, - unknown_sources=unknown_sources, - hash=version_data.get("hash"), - title=title - ) - - -@attr.s -class AddonInfo(object): - """Object matching json payload from Server""" - name = attr.ib() - versions = attr.ib(default=attr.Factory(dict)) - title = attr.ib(default=None) - description = attr.ib(default=None) - license = attr.ib(default=None) - authors = attr.ib(default=None) - - @classmethod - def from_dict(cls, data): - """Addon info by available versions. - - Args: - data (dict[str, Any]): Addon information from server. Should - contain information about every version under 'versions'. - - Returns: - AddonInfo: Addon info with available versions. - """ - - # server payload contains info about all versions - addon_name = data["name"] - title = data.get("title") or addon_name - - src_versions = data.get("versions") or {} - dst_versions = { - addon_version: AddonVersionInfo.from_dict( - addon_name, title, addon_version, version_data - ) - for addon_version, version_data in src_versions.items() - } - return cls( - name=addon_name, - versions=dst_versions, - description=data.get("description"), - title=data.get("title") or addon_name, - license=data.get("license"), - authors=data.get("authors") - ) - - -@attr.s -class DependencyItem(object): - """Object matching payload from Server about single dependency package""" - name = attr.ib() - platform_name = attr.ib() - checksum = attr.ib() - sources = attr.ib(default=attr.Factory(list)) - unknown_sources = attr.ib(default=attr.Factory(list)) - source_addons = attr.ib(default=attr.Factory(dict)) - python_modules = attr.ib(default=attr.Factory(dict)) - - @classmethod - def from_dict(cls, package): - src_sources = package.get("sources") or [] - for source in src_sources: - if source.get("type") == "server" and not source.get("filename"): - source["filename"] = package["filename"] - sources, unknown_sources = prepare_sources(src_sources) - return cls( - name=package["filename"], - platform_name=package["platform"], - sources=sources, - unknown_sources=unknown_sources, - checksum=package["checksum"], - source_addons=package["sourceAddons"], - python_modules=package["pythonModules"] - ) - - -@attr.s -class Installer: - version = attr.ib() - filename = attr.ib() - platform_name = attr.ib() - size = attr.ib() - checksum = attr.ib() - python_version = attr.ib() - python_modules = attr.ib() - sources = attr.ib(default=attr.Factory(list)) - unknown_sources = attr.ib(default=attr.Factory(list)) - - @classmethod - def from_dict(cls, installer_info): - sources, unknown_sources = prepare_sources( - installer_info.get("sources")) - - return cls( - version=installer_info["version"], - filename=installer_info["filename"], - platform_name=installer_info["platform"], - size=installer_info["size"], - sources=sources, - unknown_sources=unknown_sources, - checksum=installer_info["checksum"], - python_version=installer_info["pythonVersion"], - python_modules=installer_info["pythonModules"] - ) - - -@attr.s -class Bundle: - """Class representing bundle information.""" - - name = attr.ib() - installer_version = attr.ib() - addon_versions = attr.ib(default=attr.Factory(dict)) - dependency_packages = attr.ib(default=attr.Factory(dict)) - is_production = attr.ib(default=False) - is_staging = attr.ib(default=False) - - @classmethod - def from_dict(cls, data): - return cls( - name=data["name"], - installer_version=data.get("installerVersion"), - addon_versions=data.get("addons", {}), - dependency_packages=data.get("dependencyPackages", {}), - is_production=data["isProduction"], - is_staging=data["isStaging"], - ) diff --git a/common/ayon_common/distribution/downloaders.py b/common/ayon_common/distribution/downloaders.py deleted file mode 100644 index 23280176c3..0000000000 --- a/common/ayon_common/distribution/downloaders.py +++ /dev/null @@ -1,250 +0,0 @@ -import os -import logging -import platform -from abc import ABCMeta, abstractmethod - -import ayon_api - -from .file_handler import RemoteFileHandler -from .data_structures import UrlType - - -class SourceDownloader(metaclass=ABCMeta): - """Abstract class for source downloader.""" - - log = logging.getLogger(__name__) - - @classmethod - @abstractmethod - def download(cls, source, destination_dir, data, transfer_progress): - """Returns url of downloaded addon zip file. - - Tranfer progress can be ignored, in that case file transfer won't - be shown as 0-100% but as 'running'. First step should be to set - destination content size and then add transferred chunk sizes. - - Args: - source (dict): {type:"http", "url":"https://} ...} - destination_dir (str): local folder to unzip - data (dict): More information about download content. Always have - 'type' key in. - transfer_progress (ayon_api.TransferProgress): Progress of - transferred (copy/download) content. - - Returns: - (str) local path to addon zip file - """ - - pass - - @classmethod - @abstractmethod - def cleanup(cls, source, destination_dir, data): - """Cleanup files when distribution finishes or crashes. - - Cleanup e.g. temporary files (downloaded zip) or other related stuff - to downloader. - """ - - pass - - @classmethod - def check_hash(cls, addon_path, addon_hash, hash_type="sha256"): - """Compares 'hash' of downloaded 'addon_url' file. - - Args: - addon_path (str): Local path to addon file. - addon_hash (str): Hash of downloaded file. - hash_type (str): Type of hash. - - Raises: - ValueError if hashes doesn't match - """ - - if not os.path.exists(addon_path): - raise ValueError(f"{addon_path} doesn't exist.") - if not RemoteFileHandler.check_integrity( - addon_path, addon_hash, hash_type=hash_type - ): - raise ValueError(f"{addon_path} doesn't match expected hash.") - - @classmethod - def unzip(cls, addon_zip_path, destination_dir): - """Unzips local 'addon_zip_path' to 'destination'. - - Args: - addon_zip_path (str): local path to addon zip file - destination_dir (str): local folder to unzip - """ - - RemoteFileHandler.unzip(addon_zip_path, destination_dir) - os.remove(addon_zip_path) - - -class OSDownloader(SourceDownloader): - """Downloader using files from file drive.""" - - @classmethod - def download(cls, source, destination_dir, data, transfer_progress): - # OS doesn't need to download, unzip directly - addon_url = source["path"].get(platform.system().lower()) - if not os.path.exists(addon_url): - raise ValueError(f"{addon_url} is not accessible") - return addon_url - - @classmethod - def cleanup(cls, source, destination_dir, data): - # Nothing to do - download does not copy anything - pass - - -class HTTPDownloader(SourceDownloader): - """Downloader using http or https protocol.""" - - CHUNK_SIZE = 100000 - - @staticmethod - def get_filename(source): - source_url = source["url"] - filename = source.get("filename") - if not filename: - filename = os.path.basename(source_url) - basename, ext = os.path.splitext(filename) - allowed_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS) - if ext.lower().lstrip(".") not in allowed_exts: - filename = f"{basename}.zip" - return filename - - @classmethod - def download(cls, source, destination_dir, data, transfer_progress): - source_url = source["url"] - cls.log.debug(f"Downloading {source_url} to {destination_dir}") - headers = source.get("headers") - filename = cls.get_filename(source) - - # TODO use transfer progress - RemoteFileHandler.download_url( - source_url, - destination_dir, - filename, - headers=headers - ) - - return os.path.join(destination_dir, filename) - - @classmethod - def cleanup(cls, source, destination_dir, data): - filename = cls.get_filename(source) - filepath = os.path.join(destination_dir, filename) - if os.path.exists(filepath) and os.path.isfile(filepath): - os.remove(filepath) - - -class AyonServerDownloader(SourceDownloader): - """Downloads static resource file from AYON Server. - - Expects filled env var AYON_SERVER_URL. - """ - - CHUNK_SIZE = 8192 - - @classmethod - def download(cls, source, destination_dir, data, transfer_progress): - path = source["path"] - filename = source["filename"] - if path and not filename: - filename = path.split("/")[-1] - - cls.log.debug(f"Downloading {filename} to {destination_dir}") - - _, ext = os.path.splitext(filename) - ext = ext.lower().lstrip(".") - valid_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS) - if ext not in valid_exts: - raise ValueError(( - f"Invalid file extension \"{ext}\"." - f" Expected {', '.join(valid_exts)}" - )) - - if path: - filepath = os.path.join(destination_dir, filename) - return ayon_api.download_file( - path, - filepath, - chunk_size=cls.CHUNK_SIZE, - progress=transfer_progress - ) - - # dst_filepath = os.path.join(destination_dir, filename) - if data["type"] == "dependency_package": - return ayon_api.download_dependency_package( - data["name"], - destination_dir, - filename, - platform_name=data["platform"], - chunk_size=cls.CHUNK_SIZE, - progress=transfer_progress - ) - - if data["type"] == "addon": - return ayon_api.download_addon_private_file( - data["name"], - data["version"], - filename, - destination_dir, - chunk_size=cls.CHUNK_SIZE, - progress=transfer_progress - ) - - raise ValueError(f"Unknown type to download \"{data['type']}\"") - - @classmethod - def cleanup(cls, source, destination_dir, data): - filename = source["filename"] - filepath = os.path.join(destination_dir, filename) - if os.path.exists(filepath) and os.path.isfile(filepath): - os.remove(filepath) - - -class DownloadFactory: - """Factory for downloaders.""" - - def __init__(self): - self._downloaders = {} - - def register_format(self, downloader_type, downloader): - """Register downloader for download type. - - Args: - downloader_type (UrlType): Type of source. - downloader (SourceDownloader): Downloader which cares about - download, hash check and unzipping. - """ - - self._downloaders[downloader_type.value] = downloader - - def get_downloader(self, downloader_type): - """Registered downloader for type. - - Args: - downloader_type (UrlType): Type of source. - - Returns: - SourceDownloader: Downloader object which should care about file - distribution. - - Raises: - ValueError: If type does not have registered downloader. - """ - - if downloader := self._downloaders.get(downloader_type): - return downloader() - raise ValueError(f"{downloader_type} not implemented") - - -def get_default_download_factory(): - download_factory = DownloadFactory() - download_factory.register_format(UrlType.FILESYSTEM, OSDownloader) - download_factory.register_format(UrlType.HTTP, HTTPDownloader) - download_factory.register_format(UrlType.SERVER, AyonServerDownloader) - return download_factory diff --git a/common/ayon_common/distribution/file_handler.py b/common/ayon_common/distribution/file_handler.py deleted file mode 100644 index 07f6962c98..0000000000 --- a/common/ayon_common/distribution/file_handler.py +++ /dev/null @@ -1,289 +0,0 @@ -import os -import re -import urllib -from urllib.parse import urlparse -import urllib.request -import urllib.error -import itertools -import hashlib -import tarfile -import zipfile - -import requests - -USER_AGENT = "AYON-launcher" - - -class RemoteFileHandler: - """Download file from url, might be GDrive shareable link""" - - IMPLEMENTED_ZIP_FORMATS = { - "zip", "tar", "tgz", "tar.gz", "tar.xz", "tar.bz2" - } - - @staticmethod - def calculate_md5(fpath, chunk_size=10000): - md5 = hashlib.md5() - with open(fpath, "rb") as f: - for chunk in iter(lambda: f.read(chunk_size), b""): - md5.update(chunk) - return md5.hexdigest() - - @staticmethod - def check_md5(fpath, md5, **kwargs): - return md5 == RemoteFileHandler.calculate_md5(fpath, **kwargs) - - @staticmethod - def calculate_sha256(fpath): - """Calculate sha256 for content of the file. - - Args: - fpath (str): Path to file. - - Returns: - str: hex encoded sha256 - - """ - h = hashlib.sha256() - b = bytearray(128 * 1024) - mv = memoryview(b) - with open(fpath, "rb", buffering=0) as f: - for n in iter(lambda: f.readinto(mv), 0): - h.update(mv[:n]) - return h.hexdigest() - - @staticmethod - def check_sha256(fpath, sha256, **kwargs): - return sha256 == RemoteFileHandler.calculate_sha256(fpath, **kwargs) - - @staticmethod - def check_integrity(fpath, hash_value=None, hash_type=None): - if not os.path.isfile(fpath): - return False - if hash_value is None: - return True - if not hash_type: - raise ValueError("Provide hash type, md5 or sha256") - if hash_type == "md5": - return RemoteFileHandler.check_md5(fpath, hash_value) - if hash_type == "sha256": - return RemoteFileHandler.check_sha256(fpath, hash_value) - - @staticmethod - def download_url( - url, - root, - filename=None, - max_redirect_hops=3, - headers=None - ): - """Download a file from url and place it in root. - - Args: - url (str): URL to download file from - root (str): Directory to place downloaded file in - filename (str, optional): Name to save the file under. - If None, use the basename of the URL - max_redirect_hops (Optional[int]): Maximum number of redirect - hops allowed - headers (Optional[dict[str, str]]): Additional required headers - - Authentication etc.. - """ - - root = os.path.expanduser(root) - if not filename: - filename = os.path.basename(url) - fpath = os.path.join(root, filename) - - os.makedirs(root, exist_ok=True) - - # expand redirect chain if needed - url = RemoteFileHandler._get_redirect_url( - url, max_hops=max_redirect_hops, headers=headers) - - # check if file is located on Google Drive - file_id = RemoteFileHandler._get_google_drive_file_id(url) - if file_id is not None: - return RemoteFileHandler.download_file_from_google_drive( - file_id, root, filename) - - # download the file - try: - print(f"Downloading {url} to {fpath}") - RemoteFileHandler._urlretrieve(url, fpath, headers=headers) - except (urllib.error.URLError, IOError) as exc: - if url[:5] != "https": - raise exc - - url = url.replace("https:", "http:") - print(( - "Failed download. Trying https -> http instead." - f" Downloading {url} to {fpath}" - )) - RemoteFileHandler._urlretrieve(url, fpath, headers=headers) - - @staticmethod - def download_file_from_google_drive( - file_id, root, filename=None - ): - """Download a Google Drive file from and place it in root. - Args: - file_id (str): id of file to be downloaded - root (str): Directory to place downloaded file in - filename (str, optional): Name to save the file under. - If None, use the id of the file. - """ - # Based on https://stackoverflow.com/questions/38511444/python-download-files-from-google-drive-using-url # noqa - - url = "https://docs.google.com/uc?export=download" - - root = os.path.expanduser(root) - if not filename: - filename = file_id - fpath = os.path.join(root, filename) - - os.makedirs(root, exist_ok=True) - - if os.path.isfile(fpath) and RemoteFileHandler.check_integrity(fpath): - print(f"Using downloaded and verified file: {fpath}") - else: - session = requests.Session() - - response = session.get(url, params={"id": file_id}, stream=True) - token = RemoteFileHandler._get_confirm_token(response) - - if token: - params = {"id": file_id, "confirm": token} - response = session.get(url, params=params, stream=True) - - response_content_generator = response.iter_content(32768) - first_chunk = None - while not first_chunk: # filter out keep-alive new chunks - first_chunk = next(response_content_generator) - - if RemoteFileHandler._quota_exceeded(first_chunk): - msg = ( - f"The daily quota of the file {filename} is exceeded and " - f"it can't be downloaded. This is a limitation of " - f"Google Drive and can only be overcome by trying " - f"again later." - ) - raise RuntimeError(msg) - - RemoteFileHandler._save_response_content( - itertools.chain((first_chunk, ), - response_content_generator), fpath) - response.close() - - @staticmethod - def unzip(path, destination_path=None): - if not destination_path: - destination_path = os.path.dirname(path) - - _, archive_type = os.path.splitext(path) - archive_type = archive_type.lstrip(".") - - if archive_type in ["zip"]: - print(f"Unzipping {path}->{destination_path}") - zip_file = zipfile.ZipFile(path) - zip_file.extractall(destination_path) - zip_file.close() - - elif archive_type in [ - "tar", "tgz", "tar.gz", "tar.xz", "tar.bz2" - ]: - print(f"Unzipping {path}->{destination_path}") - if archive_type == "tar": - tar_type = "r:" - elif archive_type.endswith("xz"): - tar_type = "r:xz" - elif archive_type.endswith("gz"): - tar_type = "r:gz" - elif archive_type.endswith("bz2"): - tar_type = "r:bz2" - else: - tar_type = "r:*" - try: - tar_file = tarfile.open(path, tar_type) - except tarfile.ReadError: - raise SystemExit("corrupted archive") - tar_file.extractall(destination_path) - tar_file.close() - - @staticmethod - def _urlretrieve(url, filename, chunk_size=None, headers=None): - final_headers = {"User-Agent": USER_AGENT} - if headers: - final_headers.update(headers) - - chunk_size = chunk_size or 8192 - with open(filename, "wb") as fh: - with urllib.request.urlopen( - urllib.request.Request(url, headers=final_headers) - ) as response: - for chunk in iter(lambda: response.read(chunk_size), ""): - if not chunk: - break - fh.write(chunk) - - @staticmethod - def _get_redirect_url(url, max_hops, headers=None): - initial_url = url - final_headers = {"Method": "HEAD", "User-Agent": USER_AGENT} - if headers: - final_headers.update(headers) - for _ in range(max_hops + 1): - with urllib.request.urlopen( - urllib.request.Request(url, headers=final_headers) - ) as response: - if response.url == url or response.url is None: - return url - - return response.url - else: - raise RecursionError( - f"Request to {initial_url} exceeded {max_hops} redirects. " - f"The last redirect points to {url}." - ) - - @staticmethod - def _get_confirm_token(response): - for key, value in response.cookies.items(): - if key.startswith("download_warning"): - return value - - # handle antivirus warning for big zips - found = re.search("(confirm=)([^&.+])", response.text) - if found: - return found.groups()[1] - - return None - - @staticmethod - def _save_response_content( - response_gen, destination, - ): - with open(destination, "wb") as f: - for chunk in response_gen: - if chunk: # filter out keep-alive new chunks - f.write(chunk) - - @staticmethod - def _quota_exceeded(first_chunk): - try: - return "Google Drive - Quota exceeded" in first_chunk.decode() - except UnicodeDecodeError: - return False - - @staticmethod - def _get_google_drive_file_id(url): - parts = urlparse(url) - - if re.match(r"(drive|docs)[.]google[.]com", parts.netloc) is None: - return None - - match = re.match(r"/file/d/(?P[^/]*)", parts.path) - if match is None: - return None - - return match.group("id") diff --git a/common/ayon_common/distribution/tests/test_addon_distributtion.py b/common/ayon_common/distribution/tests/test_addon_distributtion.py deleted file mode 100644 index 3e7bd1bc6a..0000000000 --- a/common/ayon_common/distribution/tests/test_addon_distributtion.py +++ /dev/null @@ -1,248 +0,0 @@ -import os -import sys -import copy -import tempfile - - -import attr -import pytest - -current_dir = os.path.dirname(os.path.abspath(__file__)) -root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", "..", "..")) -sys.path.append(root_dir) - -from common.ayon_common.distribution.downloaders import ( - DownloadFactory, - OSDownloader, - HTTPDownloader, -) -from common.ayon_common.distribution.control import ( - AyonDistribution, - UpdateState, -) -from common.ayon_common.distribution.data_structures import ( - AddonInfo, - UrlType, -) - - -@pytest.fixture -def download_factory(): - addon_downloader = DownloadFactory() - addon_downloader.register_format(UrlType.FILESYSTEM, OSDownloader) - addon_downloader.register_format(UrlType.HTTP, HTTPDownloader) - - yield addon_downloader - - -@pytest.fixture -def http_downloader(download_factory): - yield download_factory.get_downloader(UrlType.HTTP.value) - - -@pytest.fixture -def temp_folder(): - yield tempfile.mkdtemp(prefix="ayon_test_") - - -@pytest.fixture -def sample_bundles(): - yield { - "bundles": [ - { - "name": "TestBundle", - "createdAt": "2023-06-29T00:00:00.0+00:00", - "installerVersion": None, - "addons": { - "slack": "1.0.0" - }, - "dependencyPackages": {}, - "isProduction": True, - "isStaging": False - } - ], - "productionBundle": "TestBundle", - "stagingBundle": None - } - - -@pytest.fixture -def sample_addon_info(): - yield { - "name": "slack", - "title": "Slack addon", - "versions": { - "1.0.0": { - "hasSettings": True, - "hasSiteSettings": False, - "clientPyproject": { - "tool": { - "poetry": { - "dependencies": { - "nxtools": "^1.6", - "orjson": "^3.6.7", - "typer": "^0.4.1", - "email-validator": "^1.1.3", - "python": "^3.10", - "fastapi": "^0.73.0" - } - } - } - }, - "clientSourceInfo": [ - { - "type": "http", - "path": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing", # noqa - "filename": "dummy.zip" - }, - { - "type": "filesystem", - "path": { - "windows": "P:/sources/some_file.zip", - "linux": "/mnt/srv/sources/some_file.zip", - "darwin": "/Volumes/srv/sources/some_file.zip" - } - } - ], - "frontendScopes": { - "project": { - "sidebar": "hierarchy", - } - }, - "hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658" # noqa - } - }, - "description": "" - } - - -def test_register(printer): - download_factory = DownloadFactory() - - assert len(download_factory._downloaders) == 0, "Contains registered" - - download_factory.register_format(UrlType.FILESYSTEM, OSDownloader) - assert len(download_factory._downloaders) == 1, "Should contain one" - - -def test_get_downloader(printer, download_factory): - assert download_factory.get_downloader(UrlType.FILESYSTEM.value), "Should find" # noqa - - with pytest.raises(ValueError): - download_factory.get_downloader("unknown"), "Shouldn't find" - - -def test_addon_info(printer, sample_addon_info): - """Tests parsing of expected payload from v4 server into AadonInfo.""" - valid_minimum = { - "name": "slack", - "versions": { - "1.0.0": { - "clientSourceInfo": [ - { - "type": "filesystem", - "path": { - "windows": "P:/sources/some_file.zip", - "linux": "/mnt/srv/sources/some_file.zip", - "darwin": "/Volumes/srv/sources/some_file.zip" - } - } - ] - } - } - } - - assert AddonInfo.from_dict(valid_minimum), "Missing required fields" - - addon = AddonInfo.from_dict(sample_addon_info) - assert addon, "Should be created" - assert addon.name == "slack", "Incorrect name" - assert "1.0.0" in addon.versions, "Version is not in versions" - - with pytest.raises(TypeError): - assert addon["name"], "Dict approach not implemented" - - addon_as_dict = attr.asdict(addon) - assert addon_as_dict["name"], "Dict approach should work" - - -def _get_dist_item(dist_items, name, version): - final_dist_info = next( - ( - dist_info - for dist_info in dist_items - if ( - dist_info["addon_name"] == name - and dist_info["addon_version"] == version - ) - ), - {} - ) - return final_dist_info["dist_item"] - - -def test_update_addon_state( - printer, sample_addon_info, temp_folder, download_factory, sample_bundles -): - """Tests possible cases of addon update.""" - - addon_version = list(sample_addon_info["versions"])[0] - broken_addon_info = copy.deepcopy(sample_addon_info) - - # Cause crash because of invalid hash - broken_addon_info["versions"][addon_version]["hash"] = "brokenhash" - distribution = AyonDistribution( - addon_dirpath=temp_folder, - dependency_dirpath=temp_folder, - dist_factory=download_factory, - addons_info=[broken_addon_info], - dependency_packages_info=[], - bundles_info=sample_bundles - ) - distribution.distribute() - dist_items = distribution.get_addon_dist_items() - slack_dist_item = _get_dist_item( - dist_items, - sample_addon_info["name"], - addon_version - ) - slack_state = slack_dist_item.state - assert slack_state == UpdateState.UPDATE_FAILED, ( - "Update should have failed because of wrong hash") - - # Fix cache and validate if was updated - distribution = AyonDistribution( - addon_dirpath=temp_folder, - dependency_dirpath=temp_folder, - dist_factory=download_factory, - addons_info=[sample_addon_info], - dependency_packages_info=[], - bundles_info=sample_bundles - ) - distribution.distribute() - dist_items = distribution.get_addon_dist_items() - slack_dist_item = _get_dist_item( - dist_items, - sample_addon_info["name"], - addon_version - ) - assert slack_dist_item.state == UpdateState.UPDATED, ( - "Addon should have been updated") - - # Is UPDATED without calling distribute - distribution = AyonDistribution( - addon_dirpath=temp_folder, - dependency_dirpath=temp_folder, - dist_factory=download_factory, - addons_info=[sample_addon_info], - dependency_packages_info=[], - bundles_info=sample_bundles - ) - dist_items = distribution.get_addon_dist_items() - slack_dist_item = _get_dist_item( - dist_items, - sample_addon_info["name"], - addon_version - ) - assert slack_dist_item.state == UpdateState.UPDATED, ( - "Addon should already exist") diff --git a/common/ayon_common/distribution/ui/missing_bundle_window.py b/common/ayon_common/distribution/ui/missing_bundle_window.py deleted file mode 100644 index ae7a6a2976..0000000000 --- a/common/ayon_common/distribution/ui/missing_bundle_window.py +++ /dev/null @@ -1,146 +0,0 @@ -import sys - -from qtpy import QtWidgets, QtGui - -from ayon_common import is_staging_enabled -from ayon_common.resources import ( - get_icon_path, - load_stylesheet, -) -from ayon_common.ui_utils import get_qt_app - - -class MissingBundleWindow(QtWidgets.QDialog): - default_width = 410 - default_height = 170 - - def __init__( - self, url=None, bundle_name=None, use_staging=None, parent=None - ): - super().__init__(parent) - - icon_path = get_icon_path() - icon = QtGui.QIcon(icon_path) - self.setWindowIcon(icon) - self.setWindowTitle("Missing Bundle") - - self._url = url - self._bundle_name = bundle_name - self._use_staging = use_staging - self._first_show = True - - info_label = QtWidgets.QLabel("", self) - info_label.setWordWrap(True) - - btns_widget = QtWidgets.QWidget(self) - confirm_btn = QtWidgets.QPushButton("Exit", btns_widget) - - btns_layout = QtWidgets.QHBoxLayout(btns_widget) - btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.addStretch(1) - btns_layout.addWidget(confirm_btn, 0) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(info_label, 0) - main_layout.addStretch(1) - main_layout.addWidget(btns_widget, 0) - - confirm_btn.clicked.connect(self._on_confirm_click) - - self._info_label = info_label - self._confirm_btn = confirm_btn - - self._update_label() - - def set_url(self, url): - if url == self._url: - return - self._url = url - self._update_label() - - def set_bundle_name(self, bundle_name): - if bundle_name == self._bundle_name: - return - self._bundle_name = bundle_name - self._update_label() - - def set_use_staging(self, use_staging): - if self._use_staging == use_staging: - return - self._use_staging = use_staging - self._update_label() - - def showEvent(self, event): - super().showEvent(event) - if self._first_show: - self._first_show = False - self._on_first_show() - self._recalculate_sizes() - - def resizeEvent(self, event): - super().resizeEvent(event) - self._recalculate_sizes() - - def _recalculate_sizes(self): - hint = self._confirm_btn.sizeHint() - new_width = max((hint.width(), hint.height() * 3)) - self._confirm_btn.setMinimumWidth(new_width) - - def _on_first_show(self): - self.setStyleSheet(load_stylesheet()) - self.resize(self.default_width, self.default_height) - - def _on_confirm_click(self): - self.accept() - self.close() - - def _update_label(self): - self._info_label.setText(self._get_label()) - - def _get_label(self): - url_part = f" {self._url}" if self._url else "" - - if self._bundle_name: - return ( - f"Requested release bundle {self._bundle_name}" - f" is not available on server{url_part}." - "

Try to restart AYON desktop launcher. Please" - " contact your administrator if issue persist." - ) - mode = "staging" if self._use_staging else "production" - return ( - f"No release bundle is set as {mode} on the AYON" - f" server{url_part} so there is nothing to launch." - "

Please contact your administrator" - " to resolve the issue." - ) - - -def main(): - """Show message that server does not have set bundle to use. - - It is possible to pass url as argument to show it in the message. To use - this feature, pass `--url ` as argument to this script. - """ - - url = None - bundle_name = None - if "--url" in sys.argv: - url_index = sys.argv.index("--url") + 1 - if url_index < len(sys.argv): - url = sys.argv[url_index] - - if "--bundle" in sys.argv: - bundle_index = sys.argv.index("--bundle") + 1 - if bundle_index < len(sys.argv): - bundle_name = sys.argv[bundle_index] - - use_staging = is_staging_enabled() - app = get_qt_app() - window = MissingBundleWindow(url, bundle_name, use_staging) - window.show() - app.exec_() - - -if __name__ == "__main__": - main() diff --git a/common/ayon_common/distribution/utils.py b/common/ayon_common/distribution/utils.py deleted file mode 100644 index a8b755707a..0000000000 --- a/common/ayon_common/distribution/utils.py +++ /dev/null @@ -1,90 +0,0 @@ -import os -import subprocess - -from ayon_common.utils import get_ayon_appdirs, get_ayon_launch_args - - -def get_local_dir(*subdirs): - """Get product directory in user's home directory. - - Each user on machine have own local directory where are downloaded updates, - addons etc. - - Returns: - str: Path to product local directory. - """ - - if not subdirs: - raise ValueError("Must fill dir_name if nothing else provided!") - - local_dir = get_ayon_appdirs(*subdirs) - if not os.path.isdir(local_dir): - try: - os.makedirs(local_dir) - except Exception: # TODO fix exception - raise RuntimeError(f"Cannot create {local_dir}") - - return local_dir - - -def get_addons_dir(): - """Directory where addon packages are stored. - - Path to addons is defined using python module 'appdirs' which - - The path is stored into environment variable 'AYON_ADDONS_DIR'. - Value of environment variable can be overriden, but we highly recommended - to use that option only for development purposes. - - Returns: - str: Path to directory where addons should be downloaded. - """ - - addons_dir = os.environ.get("AYON_ADDONS_DIR") - if not addons_dir: - addons_dir = get_local_dir("addons") - os.environ["AYON_ADDONS_DIR"] = addons_dir - return addons_dir - - -def get_dependencies_dir(): - """Directory where dependency packages are stored. - - Path to addons is defined using python module 'appdirs' which - - The path is stored into environment variable 'AYON_DEPENDENCIES_DIR'. - Value of environment variable can be overriden, but we highly recommended - to use that option only for development purposes. - - Returns: - str: Path to directory where dependency packages should be downloaded. - """ - - dependencies_dir = os.environ.get("AYON_DEPENDENCIES_DIR") - if not dependencies_dir: - dependencies_dir = get_local_dir("dependency_packages") - os.environ["AYON_DEPENDENCIES_DIR"] = dependencies_dir - return dependencies_dir - - -def show_missing_bundle_information(url, bundle_name=None): - """Show missing bundle information window. - - This function should be called when server does not have set bundle for - production or staging, or when bundle that should be used is not available - on server. - - Using subprocess to show the dialog. Is blocking and is waiting until - dialog is closed. - - Args: - url (str): Server url where bundle is not set. - bundle_name (Optional[str]): Name of bundle that was not found. - """ - - ui_dir = os.path.join(os.path.dirname(__file__), "ui") - script_path = os.path.join(ui_dir, "missing_bundle_window.py") - args = get_ayon_launch_args(script_path, "--skip-bootstrap", "--url", url) - if bundle_name: - args.extend(["--bundle", bundle_name]) - subprocess.call(args) diff --git a/common/ayon_common/resources/AYON.icns b/common/ayon_common/resources/AYON.icns deleted file mode 100644 index 2ec66cf3e0bb522c40209b5f1767ffd01014b0a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40634 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

H};dB$t%vn!rC`t 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 Vr*$JL34HDeq4bR}Rp~lD`afSKqZR-F diff --git a/common/ayon_common/resources/__init__.py b/common/ayon_common/resources/__init__.py deleted file mode 100644 index 2b516feff3..0000000000 --- a/common/ayon_common/resources/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -import os - -from ayon_common.utils import is_staging_enabled - -RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__)) - - -def get_resource_path(*args): - path_items = list(args) - path_items.insert(0, RESOURCES_DIR) - return os.path.sep.join(path_items) - - -def get_icon_path(): - if is_staging_enabled(): - return get_resource_path("AYON_staging.png") - return get_resource_path("AYON.png") - - -def load_stylesheet(): - stylesheet_path = get_resource_path("stylesheet.css") - - with open(stylesheet_path, "r") as stream: - content = stream.read() - return content diff --git a/common/ayon_common/resources/edit.png b/common/ayon_common/resources/edit.png deleted file mode 100644 index a5a07998a65fa0c37e2089b4da30db757e42115e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9138 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 diff --git a/common/ayon_common/resources/eye.png b/common/ayon_common/resources/eye.png deleted file mode 100644 index 5a683e29748b35b3b64a200cb9723e82df45f4b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2152 zcmai0dstIt93Gi@K}1AK5$ilhQ4HAG#Z7kHAlarv9U_+)52cRn3=U&EhwZ>N#7GK* zAtv}_f~X1RrMQNGK3E`_G;gFyW#kPtEFnU861>%SHUdqpKh8P(zW4pT=l6Sm-}jy6 zgoQ43ndmu@!C<(A$Ry$5Ii0@7zXa}AO`<^t_f`G3=Ox+lsoQqojl#IUJPsC?Dy)e-<~A9e{EIA z2qq1LIz@%4?PUQu2WliVlu2p87RQ4Ii{Ql?4G!$IKw#_O@p{Yvv6%v`A@WrELObEHO$y>1b71p>Qv?|~M!;a?Aj0(E^f7>A#^Nq7WiXsF zanP8j8p2@s=#T9iz0>8ZP<}T^%toqu=t8RV^cK;{W+B5aNrsb!i zj#T(0_uXz<(-bA}mF~T3cAV1g z`Q!@km7S*cAZ64)V9FAjd+^XsnOp`qd~uScqb=LTmL7JgneuAyqOcKHsI zId_6EJKHtG1L8K2p7W|o#06JKkN4dAK2^PY`sYoHems9H*`u;L2;Qe1qt=XRWoK4c z`=&9kMh#ub+j{HsTW6HhTip6)ZL8lR4v5`)as0-YCoL1&PUoDzxHdO^#@V&rp%2R- zuB+yu0Kf=!mq?PT=Hn*Dh_bC}42 zBhD+UzbsjhRc@WzTWj`@CfA(1(RHi*$1^4K;J~`G$2^Z7-)Oxf_?o*@>!JCD(-;^r z`2OYW?Rz-3!XuvbRSo+JR^__%tSZa@t-tu#u55D+bRgEl*?s+Lc0h);Az-T8?$|6{ zJ+Arl8p=;4AFQq(TOmE3{!#a#ika5AuQkmri;m>QzT?^#0DqM#>kQg9C5Ul7FTy@qaAA}HTsH7rzZRX#RRxAbp diff --git a/common/ayon_common/resources/stylesheet.css b/common/ayon_common/resources/stylesheet.css deleted file mode 100644 index 01e664e9e8..0000000000 --- a/common/ayon_common/resources/stylesheet.css +++ /dev/null @@ -1,84 +0,0 @@ -* { - font-size: 10pt; - font-family: "Noto Sans"; - font-weight: 450; - outline: none; -} - -QWidget { - color: #D3D8DE; - background: #2C313A; - border-radius: 0px; -} - -QWidget:disabled { - color: #5b6779; -} - -QLabel { - background: transparent; -} - -QPushButton { - text-align:center center; - border: 0px solid transparent; - border-radius: 0.2em; - padding: 3px 5px 3px 5px; - background: #434a56; -} - -QPushButton:hover { - background: rgba(168, 175, 189, 0.3); - color: #F0F2F5; -} - -QPushButton:pressed {} - -QPushButton:disabled { - background: #434a56; -} - -QLineEdit { - border: 1px solid #373D48; - border-radius: 0.3em; - background: #21252B; - padding: 0.1em; -} - -QLineEdit:disabled { - background: #2C313A; -} -QLineEdit:hover { - border-color: rgba(168, 175, 189, .3); -} -QLineEdit:focus { - border-color: rgb(92, 173, 214); -} - -QLineEdit[state="invalid"] { - border-color: #AA5050; -} - -#Separator { - background: rgba(75, 83, 98, 127); -} - -#PasswordBtn { - border: none; - padding: 0.1em; - background: transparent; -} - -#PasswordBtn:hover { - background: #434a56; -} - -#LikeDisabledInput { - background: #2C313A; -} -#LikeDisabledInput:hover { - border-color: #373D48; -} -#LikeDisabledInput:focus { - border-color: #373D48; -} diff --git a/common/ayon_common/ui_utils.py b/common/ayon_common/ui_utils.py deleted file mode 100644 index a3894d0d9c..0000000000 --- a/common/ayon_common/ui_utils.py +++ /dev/null @@ -1,36 +0,0 @@ -import sys -from qtpy import QtWidgets, QtCore - - -def set_style_property(widget, property_name, property_value): - """Set widget's property that may affect style. - - Style of widget is polished if current property value is different. - """ - - cur_value = widget.property(property_name) - if cur_value == property_value: - return - widget.setProperty(property_name, property_value) - widget.style().polish(widget) - - -def get_qt_app(): - app = QtWidgets.QApplication.instance() - if app is not None: - return app - - for attr_name in ( - "AA_EnableHighDpiScaling", - "AA_UseHighDpiPixmaps", - ): - attr = getattr(QtCore.Qt, attr_name, None) - if attr is not None: - QtWidgets.QApplication.setAttribute(attr) - - if hasattr(QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy"): - QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( - QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough - ) - - return QtWidgets.QApplication(sys.argv) diff --git a/common/ayon_common/utils.py b/common/ayon_common/utils.py deleted file mode 100644 index c0d0c7c0b1..0000000000 --- a/common/ayon_common/utils.py +++ /dev/null @@ -1,90 +0,0 @@ -import os -import sys -import appdirs - -IS_BUILT_APPLICATION = getattr(sys, "frozen", False) - - -def get_ayon_appdirs(*args): - """Local app data directory of AYON client. - - Args: - *args (Iterable[str]): Subdirectories/files in local app data dir. - - Returns: - str: Path to directory/file in local app data dir. - """ - - return os.path.join( - appdirs.user_data_dir("AYON", "Ynput"), - *args - ) - - -def is_staging_enabled(): - """Check if staging is enabled. - - Returns: - bool: True if staging is enabled. - """ - - return os.getenv("AYON_USE_STAGING") == "1" - - -def _create_local_site_id(): - """Create a local site identifier. - - Returns: - str: Randomly generated site id. - """ - - from coolname import generate_slug - - new_id = generate_slug(3) - - print("Created local site id \"{}\"".format(new_id)) - - return new_id - - -def get_local_site_id(): - """Get local site identifier. - - Site id is created if does not exist yet. - - Returns: - str: Site id. - """ - - # used for background syncing - site_id = os.environ.get("AYON_SITE_ID") - if site_id: - return site_id - - site_id_path = get_ayon_appdirs("site_id") - if os.path.exists(site_id_path): - with open(site_id_path, "r") as stream: - site_id = stream.read() - - if not site_id: - site_id = _create_local_site_id() - with open(site_id_path, "w") as stream: - stream.write(site_id) - return site_id - - -def get_ayon_launch_args(*args): - """Launch arguments that can be used to launch ayon process. - - Args: - *args (str): Additional arguments. - - Returns: - list[str]: Launch arguments. - """ - - output = [sys.executable] - if not IS_BUILT_APPLICATION: - output.append(sys.argv[0]) - output.extend(args) - return output diff --git a/openpype/vendor/python/common/ayon_api/__init__.py b/openpype/vendor/python/common/ayon_api/__init__.py index 4b4e0f3359..0540d7692d 100644 --- a/openpype/vendor/python/common/ayon_api/__init__.py +++ b/openpype/vendor/python/common/ayon_api/__init__.py @@ -78,6 +78,8 @@ from ._api import ( download_dependency_package, upload_dependency_package, + upload_addon_zip, + get_bundles, create_bundle, update_bundle, @@ -262,6 +264,8 @@ __all__ = ( "download_dependency_package", "upload_dependency_package", + "upload_addon_zip", + "get_bundles", "create_bundle", "update_bundle", diff --git a/openpype/vendor/python/common/ayon_api/_api.py b/openpype/vendor/python/common/ayon_api/_api.py index 82ffdc7527..26a4b1530a 100644 --- a/openpype/vendor/python/common/ayon_api/_api.py +++ b/openpype/vendor/python/common/ayon_api/_api.py @@ -25,12 +25,29 @@ class GlobalServerAPI(ServerAPI): but that can be filled afterwards with calling 'login' method. """ - def __init__(self, site_id=None, client_version=None): + def __init__( + self, + site_id=None, + client_version=None, + default_settings_variant=None, + ssl_verify=None, + cert=None, + ): url = self.get_url() token = self.get_token() - super(GlobalServerAPI, self).__init__(url, token, site_id, client_version) - + super(GlobalServerAPI, self).__init__( + url, + token, + site_id, + client_version, + default_settings_variant, + ssl_verify, + cert, + # We want to make sure that server and api key validation + # happens all the time in 'GlobalServerAPI'. + create_session=False, + ) self.validate_server_availability() self.create_session() @@ -129,17 +146,6 @@ class ServiceContext: addon_version = None service_name = None - @staticmethod - def get_value_from_envs(env_keys, value=None): - if value: - return value - - for env_key in env_keys: - value = os.environ.get(env_key) - if value: - break - return value - @classmethod def init_service( cls, @@ -150,14 +156,8 @@ class ServiceContext: service_name=None, connect=True ): - token = cls.get_value_from_envs( - ("AY_API_KEY", "AYON_API_KEY"), - token - ) - server_url = cls.get_value_from_envs( - ("AY_SERVER_URL", "AYON_SERVER_URL"), - server_url - ) + token = token or os.environ.get("AYON_API_KEY") + server_url = server_url or os.environ.get("AYON_SERVER_URL") if not server_url: raise FailedServiceInit("URL to server is not set") @@ -166,18 +166,9 @@ class ServiceContext: "Token to server {} is not set".format(server_url) ) - addon_name = cls.get_value_from_envs( - ("AY_ADDON_NAME", "AYON_ADDON_NAME"), - addon_name - ) - addon_version = cls.get_value_from_envs( - ("AY_ADDON_VERSION", "AYON_ADDON_VERSION"), - addon_version - ) - service_name = cls.get_value_from_envs( - ("AY_SERVICE_NAME", "AYON_SERVICE_NAME"), - service_name - ) + addon_name = addon_name or os.environ.get("AYON_ADDON_NAME") + addon_version = addon_version or os.environ.get("AYON_ADDON_VERSION") + service_name = service_name or os.environ.get("AYON_SERVICE_NAME") cls.token = token cls.server_url = server_url @@ -618,6 +609,11 @@ def delete_dependency_package(*args, **kwargs): return con.delete_dependency_package(*args, **kwargs) +def upload_addon_zip(*args, **kwargs): + con = get_server_api_connection() + return con.upload_addon_zip(*args, **kwargs) + + def get_project_anatomy_presets(*args, **kwargs): con = get_server_api_connection() return con.get_project_anatomy_presets(*args, **kwargs) diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index c886fed976..c578124cfc 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -4,7 +4,6 @@ import io import json import logging import collections -import datetime import platform import copy import uuid @@ -325,6 +324,8 @@ class ServerAPI(object): available then 'True' is used. cert (Optional[str]): Path to certificate file. Looks for env variable value 'AYON_CERT_FILE' by default. + create_session (Optional[bool]): Create session for connection if + token is available. Default is True. """ def __init__( @@ -336,6 +337,7 @@ class ServerAPI(object): default_settings_variant=None, ssl_verify=None, cert=None, + create_session=True, ): if not base_url: raise ValueError("Invalid server URL {}".format(str(base_url))) @@ -367,6 +369,7 @@ class ServerAPI(object): self._access_token_is_service = None self._token_is_valid = None + self._token_validation_started = False self._server_available = None self._server_version = None self._server_version_tuple = None @@ -389,6 +392,11 @@ class ServerAPI(object): self._as_user_stack = _AsUserStack() self._thumbnail_cache = ThumbnailCache(True) + # Create session + if self._access_token and create_session: + self.validate_server_availability() + self.create_session() + @property def log(self): if self._log is None: @@ -652,6 +660,7 @@ class ServerAPI(object): def validate_token(self): try: + self._token_validation_started = True # TODO add other possible validations # - existence of 'user' key in info # - validate that 'site_id' is in 'sites' in info @@ -661,6 +670,9 @@ class ServerAPI(object): except UnauthorizedError: self._token_is_valid = False + + finally: + self._token_validation_started = False return self._token_is_valid def set_token(self, token): @@ -673,8 +685,25 @@ class ServerAPI(object): self._token_is_valid = None self.close_session() - def create_session(self): + def create_session(self, ignore_existing=True, force=False): + """Create a connection session. + + Session helps to keep connection with server without + need to reconnect on each call. + + Args: + ignore_existing (bool): If session already exists, + ignore creation. + force (bool): If session already exists, close it and + create new. + """ + + if force and self._session is not None: + self.close_session() + if self._session is not None: + if ignore_existing: + return raise ValueError("Session is already created.") self._as_user_stack.clear() @@ -841,7 +870,19 @@ class ServerAPI(object): self._access_token) return headers - def login(self, username, password): + def login(self, username, password, create_session=True): + """Login to server. + + Args: + username (str): Username. + password (str): Password. + create_session (Optional[bool]): Create session after login. + Default: True. + + Raises: + AuthenticationError: Login failed. + """ + if self.has_valid_token: try: user_info = self.get_user() @@ -851,7 +892,8 @@ class ServerAPI(object): current_username = user_info.get("name") if current_username == username: self.close_session() - self.create_session() + if create_session: + self.create_session() return self.reset_token() @@ -875,7 +917,9 @@ class ServerAPI(object): if not self.has_valid_token: raise AuthenticationError("Invalid credentials") - self.create_session() + + if create_session: + self.create_session() def logout(self, soft=False): if self._access_token: @@ -888,6 +932,15 @@ class ServerAPI(object): def _do_rest_request(self, function, url, **kwargs): if self._session is None: + # Validate token if was not yet validated + # - ignore validation if we're in middle of + # validation + if ( + self._token_is_valid is None + and not self._token_validation_started + ): + self.validate_token() + if "headers" not in kwargs: kwargs["headers"] = self.get_headers() @@ -1328,6 +1381,7 @@ class ServerAPI(object): response = post_func(url, data=stream, **kwargs) response.raise_for_status() progress.set_transferred_size(size) + return response def upload_file( self, endpoint, filepath, progress=None, request_type=None @@ -1344,6 +1398,9 @@ class ServerAPI(object): to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. + + Returns: + requests.Response: Response object. """ if endpoint.startswith(self._base_url): @@ -1362,7 +1419,7 @@ class ServerAPI(object): progress.set_started() try: - self._upload_file(url, filepath, progress, request_type) + return self._upload_file(url, filepath, progress, request_type) except Exception as exc: progress.set_failed(str(exc)) @@ -1640,7 +1697,7 @@ class ServerAPI(object): Args: addon_name (str): Name of addon. addon_version (str): Version of addon. - subpaths (tuple[str]): Any amount of subpaths that are added to + *subpaths (str): Any amount of subpaths that are added to addon url. Returns: @@ -1848,9 +1905,12 @@ class ServerAPI(object): dst_filename (str): Destination filename. progress (Optional[TransferProgress]): Object that gives ability to track download progress. + + Returns: + requests.Response: Response object. """ - self.upload_file( + return self.upload_file( "desktop/installers/{}".format(dst_filename), src_filepath, progress=progress @@ -2162,6 +2222,33 @@ class ServerAPI(object): return create_dependency_package_basename(platform_name) + def upload_addon_zip(self, src_filepath, progress=None): + """Upload addon zip file to server. + + File is validated on server. If it is valid, it is installed. It will + create an event job which can be tracked (tracking part is not + implemented yet). + + Example output: + {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + + Args: + src_filepath (str): Path to a zip file. + progress (Optional[TransferProgress]): Object to keep track about + upload state. + + Returns: + dict[str, Any]: Response data from server. + """ + + response = self.upload_file( + "addons/install", + src_filepath, + progress=progress, + request_type=RequestTypes.post, + ) + return response.json() + def _get_bundles_route(self): major, minor, patch, _, _ = self.server_version_tuple # Backwards compatibility for AYON server 0.3.0 @@ -3051,6 +3138,65 @@ class ServerAPI(object): fill_own_attribs(project) return project + def get_folders_hierarchy( + self, + project_name, + search_string=None, + folder_types=None + ): + """Get project hierarchy. + + All folders in project in hierarchy data structure. + + Example output: + { + "hierarchy": [ + { + "id": "...", + "name": "...", + "label": "...", + "status": "...", + "folderType": "...", + "hasTasks": False, + "taskNames": [], + "parents": [], + "parentId": None, + "children": [...children folders...] + }, + ... + ] + } + + Args: + project_name (str): Project where to look for folders. + search_string (Optional[str]): Search string to filter folders. + folder_types (Optional[Iterable[str]]): Folder types to filter. + + Returns: + dict[str, Any]: Response data from server. + """ + + if folder_types: + folder_types = ",".join(folder_types) + + query_fields = [ + "{}={}".format(key, value) + for key, value in ( + ("search", search_string), + ("types", folder_types), + ) + if value + ] + query = "" + if query_fields: + query = "?{}".format(",".join(query_fields)) + + response = self.get( + "projects/{}/hierarchy{}".format(project_name, query) + ) + response.raise_for_status() + return response.data + def get_folders( self, project_name, @@ -3622,7 +3768,6 @@ class ServerAPI(object): if filtered_product is not None: yield filtered_product - def get_product_by_id( self, project_name, diff --git a/openpype/vendor/python/common/ayon_api/thumbnails.py b/openpype/vendor/python/common/ayon_api/thumbnails.py index 11734ca762..50acd94dcb 100644 --- a/openpype/vendor/python/common/ayon_api/thumbnails.py +++ b/openpype/vendor/python/common/ayon_api/thumbnails.py @@ -50,7 +50,7 @@ class ThumbnailCache: """ if self._thumbnails_dir is None: - directory = appdirs.user_data_dir("AYON", "Ynput") + directory = appdirs.user_data_dir("ayon", "ynput") self._thumbnails_dir = os.path.join(directory, "thumbnails") return self._thumbnails_dir diff --git a/openpype/vendor/python/common/ayon_api/utils.py b/openpype/vendor/python/common/ayon_api/utils.py index 69fd8e9b41..93822a58ac 100644 --- a/openpype/vendor/python/common/ayon_api/utils.py +++ b/openpype/vendor/python/common/ayon_api/utils.py @@ -359,7 +359,7 @@ class TransferProgress: def __init__(self): self._started = False self._transfer_done = False - self._transfered = 0 + self._transferred = 0 self._content_size = None self._failed = False @@ -369,25 +369,66 @@ class TransferProgress: self._destination_url = "N/A" def get_content_size(self): + """Content size in bytes. + + Returns: + Union[int, None]: Content size in bytes or None + if is unknown. + """ + return self._content_size def set_content_size(self, content_size): + """Set content size in bytes. + + Args: + content_size (int): Content size in bytes. + + Raises: + ValueError: If content size was already set. + """ + if self._content_size is not None: raise ValueError("Content size was set more then once") self._content_size = content_size def get_started(self): + """Transfer was started. + + Returns: + bool: True if transfer started. + """ + return self._started def set_started(self): + """Mark that transfer started. + + Raises: + ValueError: If transfer was already started. + """ + if self._started: raise ValueError("Progress already started") self._started = True def get_transfer_done(self): + """Transfer finished. + + Returns: + bool: Transfer finished. + """ + return self._transfer_done def set_transfer_done(self): + """Mark progress as transfer finished. + + Raises: + ValueError: If progress was already marked as done + or wasn't started yet. + """ + if self._transfer_done: raise ValueError("Progress was already marked as done") if not self._started: @@ -395,41 +436,117 @@ class TransferProgress: self._transfer_done = True def get_failed(self): + """Transfer failed. + + Returns: + bool: True if transfer failed. + """ + return self._failed def get_fail_reason(self): + """Get reason why transfer failed. + + Returns: + Union[str, None]: Reason why transfer + failed or None. + """ + return self._fail_reason def set_failed(self, reason): + """Mark progress as failed. + + Args: + reason (str): Reason why transfer failed. + """ + self._fail_reason = reason self._failed = True def get_transferred_size(self): - return self._transfered + """Already transferred size in bytes. - def set_transferred_size(self, transfered): - self._transfered = transfered + Returns: + int: Already transferred size in bytes. + """ + + return self._transferred + + def set_transferred_size(self, transferred): + """Set already transferred size in bytes. + + Args: + transferred (int): Already transferred size in bytes. + """ + + self._transferred = transferred def add_transferred_chunk(self, chunk_size): - self._transfered += chunk_size + """Add transferred chunk size in bytes. + + Args: + chunk_size (int): Add transferred chunk size + in bytes. + """ + + self._transferred += chunk_size def get_source_url(self): + """Source url from where transfer happens. + + Note: + Consider this as title. Must be set using + 'set_source_url' or 'N/A' will be returned. + + Returns: + str: Source url from where transfer happens. + """ + return self._source_url def set_source_url(self, url): + """Set source url from where transfer happens. + + Args: + url (str): Source url from where transfer happens. + """ + self._source_url = url def get_destination_url(self): + """Destination url where transfer happens. + + Note: + Consider this as title. Must be set using + 'set_source_url' or 'N/A' will be returned. + + Returns: + str: Destination url where transfer happens. + """ + return self._destination_url def set_destination_url(self, url): + """Set destination url where transfer happens. + + Args: + url (str): Destination url where transfer happens. + """ + self._destination_url = url @property def is_running(self): + """Check if transfer is running. + + Returns: + bool: True if transfer is running. + """ + if ( not self.started - or self.done + or self.transfer_done or self.failed ): return False @@ -437,9 +554,16 @@ class TransferProgress: @property def transfer_progress(self): + """Get transfer progress in percents. + + Returns: + Union[float, None]: Transfer progress in percents or 'None' + if content size is unknown. + """ + if self._content_size is None: return None - return (self._transfered * 100.0) / float(self._content_size) + return (self._transferred * 100.0) / float(self._content_size) content_size = property(get_content_size, set_content_size) started = property(get_started) @@ -448,7 +572,6 @@ class TransferProgress: fail_reason = property(get_fail_reason) source_url = property(get_source_url, set_source_url) destination_url = property(get_destination_url, set_destination_url) - content_size = property(get_content_size, set_content_size) transferred_size = property(get_transferred_size, set_transferred_size) diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index 238f6e9426..93024ea5f2 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.3.2" +__version__ = "0.3.3" diff --git a/setup.py b/setup.py index 260728dde6..4b6f286730 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Setup info for building OpenPype 3.0.""" import os -import sys import re import platform import distutils.spawn @@ -125,7 +124,6 @@ bin_includes = [ include_files = [ "igniter", "openpype", - "common", "schema", "LICENSE", "README.md" @@ -170,22 +168,7 @@ executables = [ target_name="openpype_console", icon=icon_path.as_posix() ), - Executable( - "ayon_start.py", - base=base, - target_name="ayon", - icon=icon_path.as_posix() - ), ] -if IS_WINDOWS: - executables.append( - Executable( - "ayon_start.py", - base=None, - target_name="ayon_console", - icon=icon_path.as_posix() - ) - ) if IS_LINUX: executables.append( diff --git a/tools/run_tray_ayon.ps1 b/tools/run_tray_ayon.ps1 deleted file mode 100644 index 54a80f93fd..0000000000 --- a/tools/run_tray_ayon.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -<# -.SYNOPSIS - Helper script AYON Tray. - -.DESCRIPTION - - -.EXAMPLE - -PS> .\run_tray.ps1 - -#> -$current_dir = Get-Location -$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent -$ayon_root = (Get-Item $script_dir).parent.FullName - -# Install PSWriteColor to support colorized output to terminal -$env:PSModulePath = $env:PSModulePath + ";$($ayon_root)\tools\modules\powershell" - -$env:_INSIDE_OPENPYPE_TOOL = "1" - -# make sure Poetry is in PATH -if (-not (Test-Path 'env:POETRY_HOME')) { - $env:POETRY_HOME = "$ayon_root\.poetry" -} -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" - - -Set-Location -Path $ayon_root - -Write-Color -Text ">>> ", "Reading Poetry ... " -Color Green, Gray -NoNewline -if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { - Write-Color -Text "NOT FOUND" -Color Yellow - Write-Color -Text "*** ", "We need to install Poetry create virtual env first ..." -Color Yellow, Gray - & "$ayon_root\tools\create_env.ps1" -} else { - Write-Color -Text "OK" -Color Green -} - -& "$($env:POETRY_HOME)\bin\poetry" run python "$($ayon_root)\ayon_start.py" tray --debug -Set-Location -Path $current_dir diff --git a/tools/run_tray_ayon.sh b/tools/run_tray_ayon.sh deleted file mode 100755 index 3039750b87..0000000000 --- a/tools/run_tray_ayon.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -# Run AYON Tray - -# Colors for terminal - -RST='\033[0m' # Text Reset - -# Regular Colors -Black='\033[0;30m' # Black -Red='\033[0;31m' # Red -Green='\033[0;32m' # Green -Yellow='\033[0;33m' # Yellow -Blue='\033[0;34m' # Blue -Purple='\033[0;35m' # Purple -Cyan='\033[0;36m' # Cyan -White='\033[0;37m' # White - -# Bold -BBlack='\033[1;30m' # Black -BRed='\033[1;31m' # Red -BGreen='\033[1;32m' # Green -BYellow='\033[1;33m' # Yellow -BBlue='\033[1;34m' # Blue -BPurple='\033[1;35m' # Purple -BCyan='\033[1;36m' # Cyan -BWhite='\033[1;37m' # White - -# Bold High Intensity -BIBlack='\033[1;90m' # Black -BIRed='\033[1;91m' # Red -BIGreen='\033[1;92m' # Green -BIYellow='\033[1;93m' # Yellow -BIBlue='\033[1;94m' # Blue -BIPurple='\033[1;95m' # Purple -BICyan='\033[1;96m' # Cyan -BIWhite='\033[1;97m' # White - - -############################################################################## -# Return absolute path -# Globals: -# None -# Arguments: -# Path to resolve -# Returns: -# None -############################################################################### -realpath () { - echo $(cd $(dirname "$1"); pwd)/$(basename "$1") -} - -# Main -main () { - # Directories - ayon_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) - - _inside_openpype_tool="1" - - if [[ -z $POETRY_HOME ]]; then - export POETRY_HOME="$ayon_root/.poetry" - fi - - echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" - if [ -f "$POETRY_HOME/bin/poetry" ]; then - echo -e "${BIGreen}OK${RST}" - else - echo -e "${BIYellow}NOT FOUND${RST}" - echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..." - . "$ayon_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; } - fi - - pushd "$ayon_root" > /dev/null || return > /dev/null - - echo -e "${BIGreen}>>>${RST} Running AYON Tray with debug option ..." - "$POETRY_HOME/bin/poetry" run python3 "$ayon_root/ayon_start.py" tray --debug -} - -main From 22288486b63de491fb2010d40658722552cb2107 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 27 Jul 2023 14:46:55 +0100 Subject: [PATCH 381/446] Fix call to renamed function --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index e6662e7420..1c42d7d246 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -202,7 +202,7 @@ class UnrealPrelaunchHook(PreLaunchHook): f"{self.signature} using existing built Ayon plugin from " f"{built_plugin_path}" )) - unreal_lib.move_built_plugin(engine_path, Path(built_plugin_path)) + unreal_lib.copy_built_plugin(engine_path, Path(built_plugin_path)) else: # Set "AYON_UNREAL_PLUGIN" to current process environment for # execution of `create_unreal_project` From cd7042a106d5d37ca1418878c7fe04dbbd6b219f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 27 Jul 2023 19:37:17 +0200 Subject: [PATCH 382/446] removing also settings schema with defaults for qt gui (#5306) --- .../defaults/project_settings/nuke.json | 28 ------------------- .../schemas/schema_nuke_publish.json | 20 ------------- 2 files changed, 48 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 85e3c0d3c3..b736c462ff 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -465,34 +465,6 @@ "viewer_process_override": "", "bake_viewer_process": true, "bake_viewer_input_process": true, - "reformat_node_add": false, - "reformat_node_config": [ - { - "type": "text", - "name": "type", - "value": "to format" - }, - { - "type": "text", - "name": "format", - "value": "HD_1080" - }, - { - "type": "text", - "name": "filter", - "value": "Lanczos6" - }, - { - "type": "bool", - "name": "black_outside", - "value": true - }, - { - "type": "bool", - "name": "pbb", - "value": false - } - ], "reformat_nodes_config": { "enabled": false, "reposition_nodes": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 3019c9b1b5..f006392bef 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -308,26 +308,6 @@ { "type": "separator" }, - { - "type": "label", - "label": "Currently we are supporting also multiple reposition nodes.
Older single reformat node is still supported
and if it is activated then preference will
be on it. If you want to use multiple reformat
nodes then you need to disable single reformat
node and enable multiple Reformat nodes
here." - }, - { - "type": "boolean", - "key": "reformat_node_add", - "label": "Add Reformat Node", - "default": false - }, - { - "type": "schema_template", - "name": "template_nuke_knob_inputs", - "template_data": [ - { - "label": "Reformat Node Knobs", - "key": "reformat_node_config" - } - ] - }, { "key": "reformat_nodes_config", "type": "dict", From 527a4499a8c4acbb77e04140306e2c647717a59e Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 27 Jul 2023 22:35:04 +0300 Subject: [PATCH 383/446] set selction to empty --- openpype/hosts/houdini/api/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 1e7eaa7e22..05e52e2478 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -170,6 +170,8 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): def create(self, subset_name, instance_data, pre_create_data): try: + self.selected_nodes = [] + if pre_create_data.get("use_selection"): self.selected_nodes = hou.selectedNodes() From 404079f07f02e05a0e8b4dd7fb0c21b94cd54277 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Jul 2023 13:29:14 +0800 Subject: [PATCH 384/446] fixing the bug of not being able to select the camera when using selection --- openpype/hosts/houdini/plugins/create/create_karma_rop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index 71c2bf1b28..11957e8a61 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -66,6 +66,7 @@ class CreateKarmaROP(plugin.HoudiniCreator): # we will use as render camera camera = None for node in self.selected_nodes: + camera = node.path() if node.type().name() == "cam": has_camera = pre_create_data.get("cam_res") if has_camera: From 7aba02ab7342231d07f0a21902dc174a02c96df4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Jul 2023 17:20:53 +0800 Subject: [PATCH 385/446] make sure the type of selected node is camera --- openpype/hosts/houdini/plugins/create/create_karma_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index 11957e8a61..c7a9fe0968 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -66,8 +66,8 @@ class CreateKarmaROP(plugin.HoudiniCreator): # we will use as render camera camera = None for node in self.selected_nodes: - camera = node.path() if node.type().name() == "cam": + camera = node.path() has_camera = pre_create_data.get("cam_res") if has_camera: res_x = node.evalParm("resx") From b43cac0b51f582579c0eae7508e918d2724fe5a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 28 Jul 2023 11:51:23 +0200 Subject: [PATCH 386/446] AYON: Addons creation enhancements (#5356) * updated nuke settings * added addon version to zip filename * fix Pattern type hint * added ignored subdirs for openpype * added titles to addons * type hint fix - again * modified settings conversion * updated aftereffects settings * updated blender settings * updated clockify settings * updated core settings * updated deadline settings * updated harmo settings * updated kistsu settings * updated maya settings * updated muster settings * updated royal render settings * updated timers manager settings * updated traypublisher settings * implemented conversion of rr paths * formatting fix --- openpype/settings/ayon_settings.py | 204 +++++++++--------- server_addon/aftereffects/server/__init__.py | 1 + .../server/settings/creator_plugins.py | 6 +- .../aftereffects/server/settings/main.py | 34 ++- .../server/settings/publish_plugins.py | 56 +++-- .../settings/templated_workfile_build.py | 33 +++ .../server/settings/workfile_builder.py | 2 +- server_addon/aftereffects/server/version.py | 2 +- server_addon/applications/server/__init__.py | 1 + server_addon/blender/server/settings/main.py | 10 + .../server/settings/publish_plugins.py | 12 +- server_addon/blender/server/version.py | 2 +- server_addon/clockify/server/settings.py | 3 +- server_addon/clockify/server/version.py | 2 +- server_addon/core/server/__init__.py | 1 + server_addon/core/server/settings/main.py | 4 +- server_addon/core/server/version.py | 2 +- server_addon/create_ayon_addons.py | 51 ++++- server_addon/deadline/server/settings/main.py | 6 +- server_addon/deadline/server/version.py | 2 +- server_addon/harmony/server/__init__.py | 1 + server_addon/harmony/server/settings/load.py | 20 -- server_addon/harmony/server/settings/main.py | 5 - server_addon/harmony/server/version.py | 2 +- server_addon/kitsu/server/settings.py | 7 +- server_addon/kitsu/server/version.py | 2 +- server_addon/maya/server/settings/creators.py | 2 - server_addon/maya/server/settings/main.py | 4 +- server_addon/maya/server/version.py | 2 +- server_addon/muster/server/settings.py | 6 +- server_addon/muster/server/version.py | 2 +- .../nuke/server/settings/publish_plugins.py | 32 --- server_addon/nuke/server/version.py | 2 +- server_addon/photoshop/server/__init__.py | 1 + server_addon/royal_render/server/settings.py | 25 ++- server_addon/royal_render/server/version.py | 2 +- .../timers_manager/server/settings.py | 24 ++- server_addon/timers_manager/server/version.py | 2 +- server_addon/traypublisher/server/__init__.py | 1 + .../server/settings/publish_plugins.py | 9 + server_addon/traypublisher/server/version.py | 2 +- 41 files changed, 341 insertions(+), 246 deletions(-) create mode 100644 server_addon/aftereffects/server/settings/templated_workfile_build.py delete mode 100644 server_addon/harmony/server/settings/load.py diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index d2a2afbee0..90c7f33fd2 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -161,91 +161,95 @@ def _convert_general(ayon_settings, output, default_settings): output["general"] = general -def _convert_kitsu_system_settings(ayon_settings, output): - output["modules"]["kitsu"] = { - "server": ayon_settings["kitsu"]["server"] - } +def _convert_kitsu_system_settings( + ayon_settings, output, addon_versions, default_settings +): + enabled = addon_versions.get("kitsu") is not None + kitsu_settings = default_settings["modules"]["kitsu"] + kitsu_settings["enabled"] = enabled + if enabled: + kitsu_settings["server"] = ayon_settings["kitsu"]["server"] + output["modules"]["kitsu"] = kitsu_settings -def _convert_ftrack_system_settings(ayon_settings, output, defaults): - # Ftrack contains few keys that are needed for initialization in OpenPype - # mode and some are used on different places - ftrack_settings = defaults["modules"]["ftrack"] - ftrack_settings["ftrack_server"] = ( - ayon_settings["ftrack"]["ftrack_server"]) - output["modules"]["ftrack"] = ftrack_settings - - -def _convert_shotgrid_system_settings(ayon_settings, output): - ayon_shotgrid = ayon_settings["shotgrid"] - # Skip conversion if different ayon addon is used - if "leecher_manager_url" not in ayon_shotgrid: - output["shotgrid"] = ayon_shotgrid - return - - shotgrid_settings = {} - for key in ( - "leecher_manager_url", - "leecher_backend_url", - "filter_projects_by_login", - ): - shotgrid_settings[key] = ayon_shotgrid[key] - - new_items = {} - for item in ayon_shotgrid["shotgrid_settings"]: - name = item.pop("name") - new_items[name] = item - shotgrid_settings["shotgrid_settings"] = new_items - - output["modules"]["shotgrid"] = shotgrid_settings - - -def _convert_timers_manager_system_settings(ayon_settings, output): - ayon_manager = ayon_settings["timers_manager"] - manager_settings = { - key: ayon_manager[key] - for key in { - "auto_stop", "full_time", "message_time", "disregard_publishing" - } - } +def _convert_timers_manager_system_settings( + ayon_settings, output, addon_versions, default_settings +): + enabled = addon_versions.get("timers_manager") is not None + manager_settings = default_settings["modules"]["timers_manager"] + manager_settings["enabled"] = enabled + if enabled: + ayon_manager = ayon_settings["timers_manager"] + manager_settings.update({ + key: ayon_manager[key] + for key in { + "auto_stop", + "full_time", + "message_time", + "disregard_publishing" + } + }) output["modules"]["timers_manager"] = manager_settings -def _convert_clockify_system_settings(ayon_settings, output): - output["modules"]["clockify"] = ayon_settings["clockify"] +def _convert_clockify_system_settings( + ayon_settings, output, addon_versions, default_settings +): + enabled = addon_versions.get("clockify") is not None + clockify_settings = default_settings["modules"]["clockify"] + clockify_settings["enabled"] = enabled + if enabled: + clockify_settings["workspace_name"] = ( + ayon_settings["clockify"]["workspace_name"] + ) + output["modules"]["clockify"] = clockify_settings -def _convert_deadline_system_settings(ayon_settings, output): - ayon_deadline = ayon_settings["deadline"] - deadline_settings = { - "deadline_urls": { +def _convert_deadline_system_settings( + ayon_settings, output, addon_versions, default_settings +): + enabled = addon_versions.get("deadline") is not None + deadline_settings = default_settings["modules"]["deadline"] + deadline_settings["enabled"] = enabled + if enabled: + ayon_deadline = ayon_settings["deadline"] + deadline_settings["deadline_urls"] = { item["name"]: item["value"] for item in ayon_deadline["deadline_urls"] } - } + output["modules"]["deadline"] = deadline_settings -def _convert_muster_system_settings(ayon_settings, output): - ayon_muster = ayon_settings["muster"] - templates_mapping = { - item["name"]: item["value"] - for item in ayon_muster["templates_mapping"] - } - output["modules"]["muster"] = { - "templates_mapping": templates_mapping, - "MUSTER_REST_URL": ayon_muster["MUSTER_REST_URL"] - } +def _convert_muster_system_settings( + ayon_settings, output, addon_versions, default_settings +): + enabled = addon_versions.get("muster") is not None + muster_settings = default_settings["modules"]["muster"] + muster_settings["enabled"] = enabled + if enabled: + ayon_muster = ayon_settings["muster"] + muster_settings["MUSTER_REST_URL"] = ayon_muster["MUSTER_REST_URL"] + muster_settings["templates_mapping"] = { + item["name"]: item["value"] + for item in ayon_muster["templates_mapping"] + } + output["modules"]["muster"] = muster_settings -def _convert_royalrender_system_settings(ayon_settings, output): - ayon_royalrender = ayon_settings["royalrender"] - output["modules"]["royalrender"] = { - "rr_paths": { +def _convert_royalrender_system_settings( + ayon_settings, output, addon_versions, default_settings +): + enabled = addon_versions.get("royalrender") is not None + rr_settings = default_settings["modules"]["royalrender"] + rr_settings["enabled"] = enabled + if enabled: + ayon_royalrender = ayon_settings["royalrender"] + rr_settings["rr_paths"] = { item["name"]: item["value"] for item in ayon_royalrender["rr_paths"] } - } + output["modules"]["royalrender"] = rr_settings def _convert_modules_system( @@ -253,42 +257,29 @@ def _convert_modules_system( ): # TODO add all modules # TODO add 'enabled' values - for key, func in ( - ("kitsu", _convert_kitsu_system_settings), - ("shotgrid", _convert_shotgrid_system_settings), - ("timers_manager", _convert_timers_manager_system_settings), - ("clockify", _convert_clockify_system_settings), - ("deadline", _convert_deadline_system_settings), - ("muster", _convert_muster_system_settings), - ("royalrender", _convert_royalrender_system_settings), + for func in ( + _convert_kitsu_system_settings, + _convert_timers_manager_system_settings, + _convert_clockify_system_settings, + _convert_deadline_system_settings, + _convert_muster_system_settings, + _convert_royalrender_system_settings, ): - if key in ayon_settings: - func(ayon_settings, output) + func(ayon_settings, output, addon_versions, default_settings) - if "ftrack" in ayon_settings: - _convert_ftrack_system_settings( - ayon_settings, output, default_settings) - - output_modules = output["modules"] - # TODO remove when not needed - for module_name, value in default_settings["modules"].items(): - if module_name not in output_modules: - output_modules[module_name] = value - - for module_name, value in default_settings["modules"].items(): - if "enabled" not in value or module_name not in output_modules: - continue - - ayon_module_name = module_name - if module_name == "sync_server": - ayon_module_name = "sitesync" - output_modules[module_name]["enabled"] = ( - ayon_module_name in addon_versions) - - # Missing modules conversions - # - "sync_server" -> renamed to sitesync - # - "slack" -> only 'enabled' - # - "job_queue" -> completelly missing in ayon + for module_name in ( + "sync_server", + "log_viewer", + "standalonepublish_tool", + "project_manager", + "job_queue", + "avalon", + "addon_paths", + ): + settings = default_settings["modules"][module_name] + if "enabled" in settings: + settings["enabled"] = False + output["modules"][module_name] = settings def convert_system_settings(ayon_settings, default_settings, addon_versions): @@ -724,12 +715,6 @@ def _convert_nuke_project_settings(ayon_settings, output): item_filter["subsets"] = item_filter.pop("product_names") item_filter["families"] = item_filter.pop("product_types") - item["reformat_node_config"] = _convert_nuke_knobs( - item["reformat_node_config"]) - - for node in item["reformat_nodes_config"]["reposition_nodes"]: - node["knobs"] = _convert_nuke_knobs(node["knobs"]) - name = item.pop("name") new_review_data_outputs[name] = item ayon_publish["ExtractReviewDataMov"]["outputs"] = new_review_data_outputs @@ -990,8 +975,11 @@ def _convert_royalrender_project_settings(ayon_settings, output): if "royalrender" not in ayon_settings: return ayon_royalrender = ayon_settings["royalrender"] + rr_paths = ayon_royalrender.get("selected_rr_paths", []) + output["royalrender"] = { - "publish": ayon_royalrender["publish"] + "publish": ayon_royalrender["publish"], + "rr_paths": rr_paths, } diff --git a/server_addon/aftereffects/server/__init__.py b/server_addon/aftereffects/server/__init__.py index e895c07ce1..e14e76e9db 100644 --- a/server_addon/aftereffects/server/__init__.py +++ b/server_addon/aftereffects/server/__init__.py @@ -6,6 +6,7 @@ from .version import __version__ class AfterEffects(BaseServerAddon): name = "aftereffects" + title = "AfterEffects" version = __version__ settings_model = AfterEffectsSettings diff --git a/server_addon/aftereffects/server/settings/creator_plugins.py b/server_addon/aftereffects/server/settings/creator_plugins.py index fee01bad26..ee52fadd40 100644 --- a/server_addon/aftereffects/server/settings/creator_plugins.py +++ b/server_addon/aftereffects/server/settings/creator_plugins.py @@ -5,8 +5,10 @@ from ayon_server.settings import BaseSettingsModel class CreateRenderPlugin(BaseSettingsModel): mark_for_review: bool = Field(True, title="Review") - defaults: list[str] = Field(default_factory=list, - title="Default Variants") + defaults: list[str] = Field( + default_factory=list, + title="Default Variants" + ) class AfterEffectsCreatorPlugins(BaseSettingsModel): diff --git a/server_addon/aftereffects/server/settings/main.py b/server_addon/aftereffects/server/settings/main.py index 9da872bd92..04d2e51cc9 100644 --- a/server_addon/aftereffects/server/settings/main.py +++ b/server_addon/aftereffects/server/settings/main.py @@ -3,8 +3,12 @@ from ayon_server.settings import BaseSettingsModel from .imageio import AfterEffectsImageIOModel from .creator_plugins import AfterEffectsCreatorPlugins -from .publish_plugins import AfterEffectsPublishPlugins +from .publish_plugins import ( + AfterEffectsPublishPlugins, + AE_PUBLISH_PLUGINS_DEFAULTS, +) from .workfile_builder import WorkfileBuilderPlugin +from .templated_workfile_build import TemplatedWorkfileBuildModel class AfterEffectsSettings(BaseSettingsModel): @@ -18,16 +22,18 @@ class AfterEffectsSettings(BaseSettingsModel): default_factory=AfterEffectsCreatorPlugins, title="Creator plugins" ) - publish: AfterEffectsPublishPlugins = Field( default_factory=AfterEffectsPublishPlugins, title="Publish plugins" ) - workfile_builder: WorkfileBuilderPlugin = Field( default_factory=WorkfileBuilderPlugin, title="Workfile Builder" ) + templated_workfile_build: TemplatedWorkfileBuildModel = Field( + default_factory=TemplatedWorkfileBuildModel, + title="Templated Workfile Build Settings" + ) DEFAULT_AFTEREFFECTS_SETTING = { @@ -39,24 +45,12 @@ DEFAULT_AFTEREFFECTS_SETTING = { ] } }, - "publish": { - "CollectReview": { - "enabled": True - }, - "ValidateSceneSettings": { - "enabled": True, - "optional": True, - "active": True, - "skip_resolution_check": [ - ".*" - ], - "skip_timelines_check": [ - ".*" - ] - } - }, + "publish": AE_PUBLISH_PLUGINS_DEFAULTS, "workfile_builder": { "create_first_version": False, "custom_templates": [] - } + }, + "templated_workfile_build": { + "profiles": [] + }, } diff --git a/server_addon/aftereffects/server/settings/publish_plugins.py b/server_addon/aftereffects/server/settings/publish_plugins.py index 0d90b08b5a..78445d3223 100644 --- a/server_addon/aftereffects/server/settings/publish_plugins.py +++ b/server_addon/aftereffects/server/settings/publish_plugins.py @@ -7,30 +7,62 @@ class CollectReviewPluginModel(BaseSettingsModel): enabled: bool = Field(True, title="Enabled") -class ValidateSceneSettingsPlugin(BaseSettingsModel): - """Validate naming of products and layers""" # - _isGroup = True - enabled: bool = True +class ValidateSceneSettingsModel(BaseSettingsModel): + """Validate naming of products and layers""" + + # _isGroup = True + enabled: bool = Field(True, title="Enabled") optional: bool = Field(False, title="Optional") active: bool = Field(True, title="Active") - skip_resolution_check: list[str] = Field( default_factory=list, - title="Skip Resolution Check for Tasks" + title="Skip Resolution Check for Tasks", ) - skip_timelines_check: list[str] = Field( default_factory=list, - title="Skip Timeline Check for Tasks" + title="Skip Timeline Check for Tasks", ) +class ValidateContainersModel(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + optional: bool = Field(True, title="Optional") + active: bool = Field(True, title="Active") + + class AfterEffectsPublishPlugins(BaseSettingsModel): CollectReview: CollectReviewPluginModel = Field( - default_facotory=CollectReviewPluginModel, - title="Collect Review" + default_factory=CollectReviewPluginModel, + title="Collect Review", ) - ValidateSceneSettings: ValidateSceneSettingsPlugin = Field( + ValidateSceneSettings: ValidateSceneSettingsModel = Field( + default_factory=ValidateSceneSettingsModel, title="Validate Scene Settings", - default_factory=ValidateSceneSettingsPlugin, ) + ValidateContainers: ValidateContainersModel = Field( + default_factory=ValidateContainersModel, + title="Validate Containers", + ) + + +AE_PUBLISH_PLUGINS_DEFAULTS = { + "CollectReview": { + "enabled": True + }, + "ValidateSceneSettings": { + "enabled": True, + "optional": True, + "active": True, + "skip_resolution_check": [ + ".*" + ], + "skip_timelines_check": [ + ".*" + ] + }, + "ValidateContainers": { + "enabled": True, + "optional": True, + "active": True, + } +} diff --git a/server_addon/aftereffects/server/settings/templated_workfile_build.py b/server_addon/aftereffects/server/settings/templated_workfile_build.py new file mode 100644 index 0000000000..e0245c8d06 --- /dev/null +++ b/server_addon/aftereffects/server/settings/templated_workfile_build.py @@ -0,0 +1,33 @@ +from pydantic import Field +from ayon_server.settings import ( + BaseSettingsModel, + task_types_enum, +) + + +class TemplatedWorkfileProfileModel(BaseSettingsModel): + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field( + default_factory=list, + title="Task names" + ) + path: str = Field( + title="Path to template" + ) + keep_placeholder: bool = Field( + False, + title="Keep placeholders") + create_first_version: bool = Field( + True, + title="Create first version" + ) + + +class TemplatedWorkfileBuildModel(BaseSettingsModel): + profiles: list[TemplatedWorkfileProfileModel] = Field( + default_factory=list + ) diff --git a/server_addon/aftereffects/server/settings/workfile_builder.py b/server_addon/aftereffects/server/settings/workfile_builder.py index d9d5fa41bf..d45d3f7f24 100644 --- a/server_addon/aftereffects/server/settings/workfile_builder.py +++ b/server_addon/aftereffects/server/settings/workfile_builder.py @@ -21,5 +21,5 @@ class WorkfileBuilderPlugin(BaseSettingsModel): ) custom_templates: list[CustomBuilderTemplate] = Field( - default_factory=CustomBuilderTemplate + default_factory=list ) diff --git a/server_addon/aftereffects/server/version.py b/server_addon/aftereffects/server/version.py index d4b9e2d7f3..a242f0e757 100644 --- a/server_addon/aftereffects/server/version.py +++ b/server_addon/aftereffects/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/applications/server/__init__.py b/server_addon/applications/server/__init__.py index a3fd92eb6e..fdec05006b 100644 --- a/server_addon/applications/server/__init__.py +++ b/server_addon/applications/server/__init__.py @@ -32,6 +32,7 @@ def get_enum_items_from_groups(groups): class ApplicationsAddon(BaseServerAddon): name = "applications" + title = "Applications" version = __version__ settings_model = ApplicationsAddonSettings diff --git a/server_addon/blender/server/settings/main.py b/server_addon/blender/server/settings/main.py index ec969afa93..f6118d39cd 100644 --- a/server_addon/blender/server/settings/main.py +++ b/server_addon/blender/server/settings/main.py @@ -25,6 +25,14 @@ class BlenderSettings(BaseSettingsModel): default_factory=UnitScaleSettingsModel, title="Set Unit Scale" ) + set_resolution_startup: bool = Field( + True, + title="Set Resolution on Startup" + ) + set_frames_startup: bool = Field( + True, + title="Set Start/End Frames and FPS on Startup" + ) imageio: BlenderImageIOModel = Field( default_factory=BlenderImageIOModel, title="Color Management (ImageIO)" @@ -45,6 +53,8 @@ DEFAULT_VALUES = { "apply_on_opening": False, "base_file_unit_scale": 0.01 }, + "set_frames_startup": True, + "set_resolution_startup": True, "publish": DEFAULT_BLENDER_PUBLISH_SETTINGS, "workfile_builder": { "create_first_version": False, diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 43ed3e3d0d..65dda78411 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -94,6 +94,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Extract Camera" ) + ExtractCameraABC: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Extract Camera as ABC" + ) ExtractLayout: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Extract Layout" @@ -143,7 +147,8 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "camera", "rig", "action", - "layout" + "layout", + "blendScene" ] }, "ExtractFBX": { @@ -171,6 +176,11 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": True, "active": True }, + "ExtractCameraABC": { + "enabled": True, + "optional": True, + "active": True + }, "ExtractLayout": { "enabled": True, "optional": True, diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/clockify/server/settings.py b/server_addon/clockify/server/settings.py index f6891fc5b8..9067cd4243 100644 --- a/server_addon/clockify/server/settings.py +++ b/server_addon/clockify/server/settings.py @@ -5,5 +5,6 @@ from ayon_server.settings import BaseSettingsModel class ClockifySettings(BaseSettingsModel): workspace_name: str = Field( "", - title="Workspace name" + title="Workspace name", + scope=["studio"] ) diff --git a/server_addon/clockify/server/version.py b/server_addon/clockify/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/clockify/server/version.py +++ b/server_addon/clockify/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/core/server/__init__.py b/server_addon/core/server/__init__.py index ff91f91c75..4de2b038a5 100644 --- a/server_addon/core/server/__init__.py +++ b/server_addon/core/server/__init__.py @@ -6,6 +6,7 @@ from .settings import CoreSettings, DEFAULT_VALUES class CoreAddon(BaseServerAddon): name = "core" + title = "Core" version = __version__ settings_model = CoreSettings diff --git a/server_addon/core/server/settings/main.py b/server_addon/core/server/settings/main.py index a1a86ae0a5..d19d732e71 100644 --- a/server_addon/core/server/settings/main.py +++ b/server_addon/core/server/settings/main.py @@ -49,8 +49,8 @@ class CoreImageIOBaseModel(BaseSettingsModel): class CoreSettings(BaseSettingsModel): - studio_name: str = Field("", title="Studio name") - studio_code: str = Field("", title="Studio code") + studio_name: str = Field("", title="Studio name", scope=["studio"]) + studio_code: str = Field("", title="Studio code", scope=["studio"]) environments: str = Field( "{}", title="Global environment variables", diff --git a/server_addon/core/server/version.py b/server_addon/core/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/core/server/version.py +++ b/server_addon/core/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index 3b566cec63..61dbd5c8d9 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -7,10 +7,10 @@ import zipfile import platform import collections from pathlib import Path -from typing import Any, Optional, Iterable +from typing import Any, Optional, Iterable, Pattern, List, Tuple # Patterns of directories to be skipped for server part of addon -IGNORE_DIR_PATTERNS: list[re.Pattern] = [ +IGNORE_DIR_PATTERNS: List[Pattern] = [ re.compile(pattern) for pattern in { # Skip directories starting with '.' @@ -21,7 +21,7 @@ IGNORE_DIR_PATTERNS: list[re.Pattern] = [ ] # Patterns of files to be skipped for server part of addon -IGNORE_FILE_PATTERNS: list[re.Pattern] = [ +IGNORE_FILE_PATTERNS: List[Pattern] = [ re.compile(pattern) for pattern in { # Skip files starting with '.' @@ -56,7 +56,7 @@ class ZipFileLongPaths(zipfile.ZipFile): ) -def _value_match_regexes(value: str, regexes: Iterable[re.Pattern]) -> bool: +def _value_match_regexes(value: str, regexes: Iterable[Pattern]) -> bool: return any( regex.search(value) for regex in regexes @@ -65,8 +65,9 @@ def _value_match_regexes(value: str, regexes: Iterable[re.Pattern]) -> bool: def find_files_in_subdir( src_path: str, - ignore_file_patterns: Optional[list[re.Pattern]] = None, - ignore_dir_patterns: Optional[list[re.Pattern]] = None + ignore_file_patterns: Optional[List[Pattern]] = None, + ignore_dir_patterns: Optional[List[Pattern]] = None, + ignore_subdirs: Optional[Iterable[Tuple[str]]] = None ): """Find all files to copy in subdirectories of given path. @@ -76,13 +77,15 @@ def find_files_in_subdir( Args: src_path (str): Path to directory to search in. - ignore_file_patterns (Optional[list[re.Pattern]]): List of regexes + ignore_file_patterns (Optional[List[Pattern]]): List of regexes to match files to ignore. - ignore_dir_patterns (Optional[list[re.Pattern]]): List of regexes + ignore_dir_patterns (Optional[List[Pattern]]): List of regexes to match directories to ignore. + ignore_subdirs (Optional[Iterable[Tuple[str]]]): List of + subdirectories to ignore. Returns: - list[tuple[str, str]]: List of tuples with path to file and parent + List[Tuple[str, str]]: List of tuples with path to file and parent directories relative to 'src_path'. """ @@ -98,6 +101,8 @@ def find_files_in_subdir( while hierarchy_queue: item: tuple[str, str] = hierarchy_queue.popleft() dirpath, parents = item + if ignore_subdirs and parents in ignore_subdirs: + continue for name in os.listdir(dirpath): path = os.path.join(dirpath, name) if os.path.isfile(path): @@ -133,7 +138,7 @@ def create_addon_zip( addon_version: str, keep_source: bool ): - zip_filepath = output_dir / f"{addon_name}.zip" + zip_filepath = output_dir / f"{addon_name}-{addon_version}.zip" addon_output_dir = output_dir / addon_name / addon_version with ZipFileLongPaths(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf: zipf.writestr( @@ -194,11 +199,35 @@ def create_openpype_package( (private_dir / pyproject_path.name) ) + ignored_hosts = [] + ignored_modules = [ + "ftrack", + "shotgrid", + "sync_server", + "example_addons", + "slack" + ] + # Subdirs that won't be added to output zip file + ignored_subpaths = [ + ["addons"], + ["vendor", "common", "ayon_api"], + ] + ignored_subpaths.extend( + ["hosts", host_name] + for host_name in ignored_hosts + ) + ignored_subpaths.extend( + ["modules", module_name] + for module_name in ignored_modules + ) + # Zip client zip_filepath = private_dir / "client.zip" with ZipFileLongPaths(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf: # Add client code content to zip - for path, sub_path in find_files_in_subdir(str(openpype_dir)): + for path, sub_path in find_files_in_subdir( + str(openpype_dir), ignore_subdirs=ignored_subpaths + ): zipf.write(path, f"{openpype_dir.name}/{sub_path}") if create_zip: diff --git a/server_addon/deadline/server/settings/main.py b/server_addon/deadline/server/settings/main.py index e60df2eda3..f158b7464d 100644 --- a/server_addon/deadline/server/settings/main.py +++ b/server_addon/deadline/server/settings/main.py @@ -18,12 +18,12 @@ class DeadlineSettings(BaseSettingsModel): deadline_urls: list[ServerListSubmodel] = Field( default_factory=list, title="System Deadline Webservice URLs", + scope=["studio"], ) - deadline_servers: list[str] = Field( title="Project deadline servers", - section="---") - + section="---", + ) publish: PublishPluginsModel = Field( default_factory=PublishPluginsModel, title="Publish Plugins", diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/harmony/server/__init__.py b/server_addon/harmony/server/__init__.py index 64f41849ad..4ecda1989e 100644 --- a/server_addon/harmony/server/__init__.py +++ b/server_addon/harmony/server/__init__.py @@ -6,6 +6,7 @@ from .version import __version__ class Harmony(BaseServerAddon): name = "harmony" + title = "Harmony" version = __version__ settings_model = HarmonySettings diff --git a/server_addon/harmony/server/settings/load.py b/server_addon/harmony/server/settings/load.py deleted file mode 100644 index 1222485ff9..0000000000 --- a/server_addon/harmony/server/settings/load.py +++ /dev/null @@ -1,20 +0,0 @@ -from pydantic import Field -from ayon_server.settings import BaseSettingsModel - - -class ImageSequenceLoaderModel(BaseSettingsModel): - family: list[str] = Field( - default_factory=list, - title="Families" - ) - representations: list[str] = Field( - default_factory=list, - title="Representations" - ) - - -class HarmonyLoadModel(BaseSettingsModel): - ImageSequenceLoader: ImageSequenceLoaderModel = Field( - default_factory=ImageSequenceLoaderModel, - title="Load Image Sequence" - ) diff --git a/server_addon/harmony/server/settings/main.py b/server_addon/harmony/server/settings/main.py index ae08da0198..0936bc1fc7 100644 --- a/server_addon/harmony/server/settings/main.py +++ b/server_addon/harmony/server/settings/main.py @@ -2,7 +2,6 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel from .imageio import HarmonyImageIOModel -from .load import HarmonyLoadModel from .publish_plugins import HarmonyPublishPlugins @@ -13,10 +12,6 @@ class HarmonySettings(BaseSettingsModel): default_factory=HarmonyImageIOModel, title="OCIO config" ) - load: HarmonyLoadModel = Field( - default_factory=HarmonyLoadModel, - title="Loader plugins" - ) publish: HarmonyPublishPlugins = Field( default_factory=HarmonyPublishPlugins, title="Publish plugins" diff --git a/server_addon/harmony/server/version.py b/server_addon/harmony/server/version.py index a242f0e757..df0c92f1e2 100644 --- a/server_addon/harmony/server/version.py +++ b/server_addon/harmony/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/server_addon/kitsu/server/settings.py b/server_addon/kitsu/server/settings.py index 7afa73ec72..a4d10d889d 100644 --- a/server_addon/kitsu/server/settings.py +++ b/server_addon/kitsu/server/settings.py @@ -76,15 +76,16 @@ class PublishPlugins(BaseSettingsModel): class KitsuSettings(BaseSettingsModel): server: str = Field( "", - title="Kitsu Server" + title="Kitsu Server", + scope=["studio"], ) entities_naming_pattern: EntityPattern = Field( default_factory=EntityPattern, - title="Entities naming pattern" + title="Entities naming pattern", ) publish: PublishPlugins = Field( default_factory=PublishPlugins, - title="Publish plugins" + title="Publish plugins", ) diff --git a/server_addon/kitsu/server/version.py b/server_addon/kitsu/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/kitsu/server/version.py +++ b/server_addon/kitsu/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 3756d45e6c..291b3ec660 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -55,7 +55,6 @@ class BasicExportMeshModel(BaseSettingsModel): class CreateAnimationModel(BaseSettingsModel): - enabled: bool = Field(title="Enabled") write_color_sets: bool = Field(title="Write Color Sets") write_face_sets: bool = Field(title="Write Face Sets") include_parent_hierarchy: bool = Field( @@ -259,7 +258,6 @@ DEFAULT_CREATORS_SETTINGS = { "publish_mip_map": True }, "CreateAnimation": { - "enabled": False, "write_color_sets": False, "write_face_sets": False, "include_parent_hierarchy": False, diff --git a/server_addon/maya/server/settings/main.py b/server_addon/maya/server/settings/main.py index 47f4121584..c8021614be 100644 --- a/server_addon/maya/server/settings/main.py +++ b/server_addon/maya/server/settings/main.py @@ -60,7 +60,9 @@ class MayaSettings(BaseSettingsModel): title="Include/Exclude Handles in default playback & render range" ) scriptsmenu: ScriptsmenuModel = Field( - default_factory=ScriptsmenuModel, title="Scriptsmenu Settings") + default_factory=ScriptsmenuModel, + title="Scriptsmenu Settings" + ) render_settings: RenderSettingsModel = Field( default_factory=RenderSettingsModel, title="Render Settings") create: CreatorsModel = Field( diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index d4b9e2d7f3..a242f0e757 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/muster/server/settings.py b/server_addon/muster/server/settings.py index f3f6660abc..e37c762870 100644 --- a/server_addon/muster/server/settings.py +++ b/server_addon/muster/server/settings.py @@ -10,7 +10,11 @@ class TemplatesMapping(BaseSettingsModel): class MusterSettings(BaseSettingsModel): enabled: bool = True - MUSTER_REST_URL: str = Field("", title="Muster Rest URL") + MUSTER_REST_URL: str = Field( + "", + title="Muster Rest URL", + scope=["studio"], + ) templates_mapping: list[TemplatesMapping] = Field( default_factory=list, diff --git a/server_addon/muster/server/version.py b/server_addon/muster/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/muster/server/version.py +++ b/server_addon/muster/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index f057fd629d..7e898f8c9a 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -165,10 +165,6 @@ class BakingStreamModel(BaseSettingsModel): viewer_process_override: str = Field(title="Viewer process override") bake_viewer_process: bool = Field(title="Bake view process") bake_viewer_input_process: bool = Field(title="Bake viewer input process") - reformat_node_add: bool = Field(title="Add reformat node") - reformat_node_config: list[KnobModel] = Field( - default_factory=list, - title="Reformat node properties") reformat_nodes_config: ReformatNodesConfigModel = Field( default_factory=ReformatNodesConfigModel, title="Reformat Nodes") @@ -443,34 +439,6 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "viewer_process_override": "", "bake_viewer_process": True, "bake_viewer_input_process": True, - "reformat_node_add": False, - "reformat_node_config": [ - { - "type": "text", - "name": "type", - "text": "to format" - }, - { - "type": "text", - "name": "format", - "text": "HD_1080" - }, - { - "type": "text", - "name": "filter", - "text": "Lanczos6" - }, - { - "type": "boolean", - "name": "black_outside", - "boolean": True - }, - { - "type": "boolean", - "name": "pbb", - "boolean": False - } - ], "reformat_nodes_config": { "enabled": False, "reposition_nodes": [ diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/photoshop/server/__init__.py b/server_addon/photoshop/server/__init__.py index e7ac218b5a..3a45f7a809 100644 --- a/server_addon/photoshop/server/__init__.py +++ b/server_addon/photoshop/server/__init__.py @@ -6,6 +6,7 @@ from .version import __version__ class Photoshop(BaseServerAddon): name = "photoshop" + title = "Photoshop" version = __version__ settings_model = PhotoshopSettings diff --git a/server_addon/royal_render/server/settings.py b/server_addon/royal_render/server/settings.py index 8b1fde6493..677d7e2671 100644 --- a/server_addon/royal_render/server/settings.py +++ b/server_addon/royal_render/server/settings.py @@ -2,11 +2,15 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel, MultiplatformPathModel +class CustomPath(MultiplatformPathModel): + _layout = "expanded" + + class ServerListSubmodel(BaseSettingsModel): - _layout = "compact" + _layout = "expanded" name: str = Field("", title="Name") - value: MultiplatformPathModel = Field( - default_factory=MultiplatformPathModel + value: CustomPath = Field( + default_factory=CustomPath ) @@ -23,13 +27,25 @@ class PublishPluginsModel(BaseSettingsModel): class RoyalRenderSettings(BaseSettingsModel): enabled: bool = True + # WARNING/TODO this needs change + # - both system and project settings contained 'rr_path' + # where project settings did choose one of rr_path from system settings + # that is not possible in AYON rr_paths: list[ServerListSubmodel] = Field( default_factory=list, title="Royal Render Root Paths", + scope=["studio"], + ) + # This was 'rr_paths' in project settings and should be enum of + # 'rr_paths' from system settings, but that's not possible in AYON + selected_rr_paths: list[str] = Field( + default_factory=list, + title="Selected Royal Render Paths", + section="---", ) publish: PublishPluginsModel = Field( default_factory=PublishPluginsModel, - title="Publish plugins" + title="Publish plugins", ) @@ -45,6 +61,7 @@ DEFAULT_VALUES = { } } ], + "selected_rr_paths": ["default"], "publish": { "CollectSequencesFromJob": { "review": True diff --git a/server_addon/royal_render/server/version.py b/server_addon/royal_render/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/royal_render/server/version.py +++ b/server_addon/royal_render/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/timers_manager/server/settings.py b/server_addon/timers_manager/server/settings.py index 27dbc6ef8e..a5c5721a57 100644 --- a/server_addon/timers_manager/server/settings.py +++ b/server_addon/timers_manager/server/settings.py @@ -3,7 +3,23 @@ from ayon_server.settings import BaseSettingsModel class TimersManagerSettings(BaseSettingsModel): - auto_stop: bool = Field(True, title="Auto stop timer") - full_time: int = Field(15, title="Max idle time") - message_time: float = Field(0.5, title="When dialog will show") - disregard_publishing: bool = Field(False, title="Disregard publishing") + auto_stop: bool = Field( + True, + title="Auto stop timer", + scope=["studio"], + ) + full_time: int = Field( + 15, + title="Max idle time", + scope=["studio"], + ) + message_time: float = Field( + 0.5, + title="When dialog will show", + scope=["studio"], + ) + disregard_publishing: bool = Field( + False, + title="Disregard publishing", + scope=["studio"], + ) diff --git a/server_addon/timers_manager/server/version.py b/server_addon/timers_manager/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/timers_manager/server/version.py +++ b/server_addon/timers_manager/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/traypublisher/server/__init__.py b/server_addon/traypublisher/server/__init__.py index 308f32069f..e6f079609f 100644 --- a/server_addon/traypublisher/server/__init__.py +++ b/server_addon/traypublisher/server/__init__.py @@ -6,6 +6,7 @@ from .settings import TraypublisherSettings, DEFAULT_TRAYPUBLISHER_SETTING class Traypublisher(BaseServerAddon): name = "traypublisher" + title = "TrayPublisher" version = __version__ settings_model = TraypublisherSettings diff --git a/server_addon/traypublisher/server/settings/publish_plugins.py b/server_addon/traypublisher/server/settings/publish_plugins.py index 3f00f3d52e..8c844f29f2 100644 --- a/server_addon/traypublisher/server/settings/publish_plugins.py +++ b/server_addon/traypublisher/server/settings/publish_plugins.py @@ -17,6 +17,10 @@ class ValidateFrameRangeModel(ValidatePluginModel): class TrayPublisherPublishPlugins(BaseSettingsModel): + CollectFrameDataFromAssetEntity: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Collect Frame Data From Folder Entity", + ) ValidateFrameRange: ValidateFrameRangeModel = Field( title="Validate Frame Range", default_factory=ValidateFrameRangeModel, @@ -28,6 +32,11 @@ class TrayPublisherPublishPlugins(BaseSettingsModel): DEFAULT_PUBLISH_PLUGINS = { + "CollectFrameDataFromAssetEntity": { + "enabled": True, + "optional": True, + "active": True + }, "ValidateFrameRange": { "enabled": True, "optional": True, diff --git a/server_addon/traypublisher/server/version.py b/server_addon/traypublisher/server/version.py index a242f0e757..df0c92f1e2 100644 --- a/server_addon/traypublisher/server/version.py +++ b/server_addon/traypublisher/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.1" +__version__ = "0.1.2" From 0d9ea4aa266a25d35f555a0b7eb1e1d13aa1f41e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 28 Jul 2023 15:46:32 +0200 Subject: [PATCH 387/446] Applications: Environment variables order (#5245) * apply project environemnts after context environments are set * make asset and task environments optional * added more conditions for host environemnts * validate context for host * fix double negative --- openpype/lib/applications.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index f47e11926c..fbde59ced5 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1640,11 +1640,7 @@ def prepare_context_environments(data, env_group=None, modules_manager=None): project_doc = data["project_doc"] asset_doc = data["asset_doc"] task_name = data["task_name"] - if ( - not project_doc - or not asset_doc - or not task_name - ): + if not project_doc: log.info( "Skipping context environments preparation." " Launch context does not contain required data." @@ -1657,18 +1653,16 @@ def prepare_context_environments(data, env_group=None, modules_manager=None): system_settings = get_system_settings() data["project_settings"] = project_settings data["system_settings"] = system_settings - # Apply project specific environments on current env value - apply_project_environments_value( - project_name, data["env"], project_settings, env_group - ) app = data["app"] context_env = { "AVALON_PROJECT": project_doc["name"], - "AVALON_ASSET": asset_doc["name"], - "AVALON_TASK": task_name, "AVALON_APP_NAME": app.full_name } + if asset_doc: + context_env["AVALON_ASSET"] = asset_doc["name"] + if task_name: + context_env["AVALON_TASK"] = task_name log.debug( "Context environments set:\n{}".format( @@ -1676,9 +1670,25 @@ def prepare_context_environments(data, env_group=None, modules_manager=None): ) ) data["env"].update(context_env) + + # Apply project specific environments on current env value + # - apply them once the context environments are set + apply_project_environments_value( + project_name, data["env"], project_settings, env_group + ) + if not app.is_host: return + data["env"]["AVALON_APP"] = app.host_name + + if not asset_doc or not task_name: + # QUESTION replace with log.info and skip workfile discovery? + # - technically it should be possible to launch host without context + raise ApplicationLaunchFailed( + "Host launch require asset and task context." + ) + workdir_data = get_template_data( project_doc, asset_doc, task_name, app.host_name, system_settings ) @@ -1716,7 +1726,6 @@ def prepare_context_environments(data, env_group=None, modules_manager=None): "Couldn't create workdir because: {}".format(str(exc)) ) - data["env"]["AVALON_APP"] = app.host_name data["env"]["AVALON_WORKDIR"] = workdir _prepare_last_workfile(data, workdir, modules_manager) From 4247fce5a97b43298946ef2550aa0b7613a822c9 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 29 Jul 2023 03:24:13 +0000 Subject: [PATCH 388/446] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 0a0b192892..61bb0f8288 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.3-nightly.1" +__version__ = "3.16.3-nightly.2" From 9a06b4f5912ce30165c6fbe8aaeb7e5b048d2597 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 29 Jul 2023 03:25:00 +0000 Subject: [PATCH 389/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c71822db2d..387b5574ab 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.3-nightly.2 - 3.16.3-nightly.1 - 3.16.2 - 3.16.2-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.7-nightly.3 - 3.14.7-nightly.2 - 3.14.7-nightly.1 - - 3.14.6 validations: required: true - type: dropdown From 20987f82de9aa4a0eb41fafb09cbb75eee7f50f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Jul 2023 10:53:50 +0200 Subject: [PATCH 390/446] move unreal splash screen to unreal --- .../hosts/unreal/hooks/pre_workfile_preparation.py | 12 ++++++------ openpype/hosts/unreal/ui/__init__.py | 5 +++++ .../{widgets => hosts/unreal/ui}/splash_screen.py | 3 +-- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 openpype/hosts/unreal/ui/__init__.py rename openpype/{widgets => hosts/unreal/ui}/splash_screen.py (98%) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 1c42d7d246..e5010366b8 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -3,21 +3,21 @@ import os import copy from pathlib import Path -from openpype.widgets.splash_screen import SplashScreen + from qtpy import QtCore -from openpype.hosts.unreal.ue_workers import ( - UEProjectGenerationWorker, - UEPluginInstallWorker -) from openpype import resources from openpype.lib import ( PreLaunchHook, ApplicationLaunchFailed, - ApplicationNotFound, ) from openpype.pipeline.workfile import get_workfile_template_key import openpype.hosts.unreal.lib as unreal_lib +from openpype.hosts.unreal.ue_workers import ( + UEProjectGenerationWorker, + UEPluginInstallWorker +) +from openpype.hosts.unreal.ui import SplashScreen class UnrealPrelaunchHook(PreLaunchHook): diff --git a/openpype/hosts/unreal/ui/__init__.py b/openpype/hosts/unreal/ui/__init__.py new file mode 100644 index 0000000000..606b21ef19 --- /dev/null +++ b/openpype/hosts/unreal/ui/__init__.py @@ -0,0 +1,5 @@ +from .splash_screen import SplashScreen + +__all__ = ( + "SplashScreen", +) diff --git a/openpype/widgets/splash_screen.py b/openpype/hosts/unreal/ui/splash_screen.py similarity index 98% rename from openpype/widgets/splash_screen.py rename to openpype/hosts/unreal/ui/splash_screen.py index 7c1ff72ecd..7ac77821d9 100644 --- a/openpype/widgets/splash_screen.py +++ b/openpype/hosts/unreal/ui/splash_screen.py @@ -1,6 +1,5 @@ from qtpy import QtWidgets, QtCore, QtGui from openpype import style, resources -from igniter.nice_progress_bar import NiceProgressBar class SplashScreen(QtWidgets.QDialog): @@ -143,7 +142,7 @@ class SplashScreen(QtWidgets.QDialog): button_layout.addWidget(self.close_btn) # Progress Bar - self.progress_bar = NiceProgressBar() + self.progress_bar = QtWidgets.QProgressBar() self.progress_bar.setValue(0) self.progress_bar.setAlignment(QtCore.Qt.AlignTop) From 40037b050ce287f503bf4214f63e9e9f1b8028f5 Mon Sep 17 00:00:00 2001 From: Mustafa Zarkash Date: Mon, 31 Jul 2023 16:01:18 +0300 Subject: [PATCH 391/446] update defaults variables (#5368) --- openpype/hosts/maya/plugins/create/create_model.py | 2 +- openpype/hosts/maya/plugins/create/create_setdress.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_model.py b/openpype/hosts/maya/plugins/create/create_model.py index 30f1a82281..5c3dd04af0 100644 --- a/openpype/hosts/maya/plugins/create/create_model.py +++ b/openpype/hosts/maya/plugins/create/create_model.py @@ -12,7 +12,7 @@ class CreateModel(plugin.MayaCreator): label = "Model" family = "model" icon = "cube" - defaults = ["Main", "Proxy", "_MD", "_HD", "_LD"] + default_variants = ["Main", "Proxy", "_MD", "_HD", "_LD"] write_color_sets = False write_face_sets = False diff --git a/openpype/hosts/maya/plugins/create/create_setdress.py b/openpype/hosts/maya/plugins/create/create_setdress.py index 594a3dc46d..23a706380a 100644 --- a/openpype/hosts/maya/plugins/create/create_setdress.py +++ b/openpype/hosts/maya/plugins/create/create_setdress.py @@ -9,7 +9,7 @@ class CreateSetDress(plugin.MayaCreator): label = "Set Dress" family = "setdress" icon = "cubes" - defaults = ["Main", "Anim"] + default_variants = ["Main", "Anim"] def get_instance_attr_defs(self): return [ From 8d1b28f8d71af82af8d68c643cfd9d49511ffed1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Jul 2023 15:14:50 +0200 Subject: [PATCH 392/446] updated ayon staging icons --- .../resources/icons/AYON_icon_staging.png | Bin 15273 -> 11268 bytes .../resources/icons/AYON_splash_staging.png | Bin 20527 -> 22076 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/openpype/resources/icons/AYON_icon_staging.png b/openpype/resources/icons/AYON_icon_staging.png index 75dadfd56c812d3bee941e90ecdd7f9f18831760..9da5b0488e25f3f15a85b625a0adeac726f7fe05 100644 GIT binary patch literal 11268 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 Hb_)N0?04LC literal 15273 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 Vr*$JL34HDeq4bR}Rp~lD`afSKqZR-F diff --git a/openpype/resources/icons/AYON_splash_staging.png b/openpype/resources/icons/AYON_splash_staging.png index 2923413664e8d14b37ee4344b928656b2bb6c0b8..ab2537e8a8b1d9751868a57768176dd3287389a1 100644 GIT binary patch literal 22076 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 literal 20527 zcmd>m`9G9v{P!SK1}!K1a;ia#HH@{Ka>7_cw=MhFjeQxipNfj1Ze?FmNp9IIJ5v+R zBujQ>84-oXAWPQgn$GumeV_l~dA<0-%za;<>-wzk&vLnQ#aNg7pwK}S3dN1n``ZkK z+NX>{v3Tw00G}MZ=i~zZi1XCez7k}nd+ATyWo@j28Wt;aPF4YhI(0wfeuDwq#X|`# z7>-uX!!NH?+x0qF=briNRv*IVvEBn0*2|SInsOFPQ=g`XYa?eBS8PJ8e? zVyH9$nH=!fvkpR`xQ;S^Sx{Ly0w~m_5YY&Yn8cI=rkHicd6EDj7CfIDglQeB|1K)n8XUhZa*J&|I<= z-SiKU^vv*s>mP@!z+y%x`H`d0Z^>abpeDIX5O)iRB@YFEwt1rPC4$ z<1sQ7FWiM`dNk~qHTKOCndBup@r~yM;LBI}H02@qY3Irbz9&7XS`>r+V)pIWMeyEB z5ou+N=SA1^yqsb~xp-39zDAk(k=MW1!8XIhI=}D>p*Y=}S`wI4er=ySjpM@1j~ocI zAts-$6Z0>pA7UQamU$?vwHt-%=cmap+#CM;)R6hDphyN|1$y@?emDhqQ`<#o9U;74> z2rPhSE-+a(#^Oe>b}wMQ9?xWeg7ntqJ?`qf+y-stXO&i9$$wqX&$9S_%*D4IthFQV z2ZEL11A-N5F<|)|oRD$DuKqXq-_LH?AsXbI7w@MY0QNj!zP(ZUzI7MTGzPC3-2r?g zO(29vo`5?1RJjtU&(gJri966~Kc-qpfW0b3YlLuSq;iFDGCA!0`;o11VyxOPYa;4@ zX^RuVLr{1Q;9b^{-@Ae8hWBv)?<3#?hlk5L;0beQKyRw%4F6WivMR8T{~@HsV*HIq zfwMjMe@Wdh3CTGqG6K%JyFao*<0jY@Um{cb@h9uqJ`5*#iMGuDCjsV?iIaM?e)=KB zpJj9uQ;7fZ8a%^t;ZrVNhjm-`KX!>R?LcI4fk*yiep$%&%m3C*VXm7zq?mKFionCP z?f;Q0*WB;}+uKa#RclA4RG7e0zQbLFyX7XC*wn@KZ+8c5n=(^~ARVi&WX->~@a4J`AGzg{Ja zqllcw^sXvFp5l`zepDbw zd5ac>aNY-^3#0?)zJ z3$@?h6^BHM4~y3FGRA;2;-vi|ACpmHs+RZfMblOZoSH|%)crqN^RPGasXiK9q6j9R z1h4OiGmp<=-OV1Gz(#b>XjLff-V;aZqRL-{h@3CXKMkJoVJe!9f_1k=XB6 zXyarvx-qff!-jt8$v&e~%!h=)L*}V2Qna6ldb36%)@ZQ$q`w-?%>7poj#fiXk&1~v z_iL8dTU5*YK#8(}8QB$iFL4o^4h}Yk8KZOD zV|s(s$ZMh>I6_Zil2KB}t*K^T!c({qHh5t;7qY>onl1U_nxkYFDKS5~&VYq*Phgh> z%!FEcVzJh*i}04qaO0I;KK;N|jQHrgbq9E1JlC4Jmd;6P>Y)F{RjGWvz$W@a?_>SI zNzC4Z>3-$r^$X%EOAnd5(yI!Bk7A$n>%|+P_avwi_2LOHPr-!LX$4&J=MU?4u;$aP z0OkqJ8ZhM}|8SLPsa>Ep<6bXDYxZVH)EU7~iWqrYo-kLimKbwyUa8X@=C!q-dTQWN z33|=(TpWB>QqB4OV$E!bmaI%OS~0=4>uKTSymg7Apu?=ariUy{;2b?rd%8ly|Cl4E zV+2i==1sPYI~ki|G4S2U^wj~$R!UKWxox_!JjdItr`wKd{$IqAND>ine zu#R&#wjjn{cTbb9!CaEz&aimtB+r+1MZ$&efj95#2ksS*$&J^Vr;6lFza2|~vT88b zB*OLJ{0|#ZYhCnqE>q zb*H9XgU?EYUmJY=ak>4=cT<}~g2~rgOkn&FLrCj1^*BzpH`hVvA*RN7c}lr4vuiE` zQ$vpyQLjcj+#NDA^SLtxZ)?Lo_2M;3ytvSA|4Y=y(Pvu<-q>@I#ZAs|g8M1CX!-V` zfizVK>QHKPy>3;Cl%fvQi>C%#q;~&1RrFIm28#c|_H-b%peO9j zgKlUMbRoTHE62ErPyU{8G8nA7_R`|VV64Y>bA%FT^+xh^$KA+Pk4{e~i_|VCM zqChGy2v?V(x~Gafn0_5lcem?u{H3YS=Fy^J`P0wFQj9iJF}xX(7mWKjzmRxKAER}H zy58TY7oR{nDmY$!7Ecw)a5{R~^SJA&*gfo2ZFaQnXp5VZOU9*`S&X02a>Afxm+JNj zYGSIb*7$ahHD*M%FS{VXLh7Bf_Q`wGI+jf6l+v^Z` zG%`M>CVNT>Czd0jnWo<%L(w5sZ^BJ=_MH_Z zqT$rN=?cM?8EI_)8LDySPTLpWZ_zokZ93P??@zv6K1wFJYxWn#W?zNeG?TIST)b&q z9H|_0>zzefjzKrfZt~?gy>LsYbQf#ajy!^_O=IW1@bZ zQCFbO!?sVh7A;mDPX7ps%vHl{2cCTvdC2#S=6A}-{i(>$Ju315s(IGprJwg#bdOh*dmYSv0CGx z9M@G@S{=0di~eU;DJC5ijs6gH2T{Z-S>o0>^OTDwn;oF^>^>{+8Jl3AO0+q$sM>y9 zj(b6+eaT{MS6ZilrQ#%mKoiE%1sByzaBvChBKu&q>MaiPap3xRD)pHjau;~18hn9* z0g>%Ox)a}r&U%W+Gy41O@}Mrx#vwuBtS1ef0Wa*}qyRy6$KrzTwTG)6%EK(SBu8Ak z@MSsZc~fzKW)&A$w}lxW6=G1{zc6)IU`p(hSdHM>5wxX?!o&)f6o?XHJR)q3hih$u zoM&ZjcPuoQ9&dYFa+4RE>3nt)Y)1>%>-^|o#5CN_rEoxSF-So zyd~h~w{X~`&qlIAIoYSOAiQ?@Zc-g?n=z;qyz^z_T2V@uF|5H#q<1N>Ut>u@H9vG& z#C#6n>P8qf^L9dU_D?V0f_Y~4S2DhcNZsCrI@5v`T%nsxF!dWduUK%^#Bxq;$w zCe&AK!dbgIwD_1{P^y)ehmg91Zi2h2?^W{$F4y8axznlaaoVv@ByW*kUWRQiP+Js5 zMC%Crmw1f=cj$*YPEe1P>7_4b-Q#Q@-%{TkKeFsiGuk-sR4q{_O1UH1 z?L5q>pXlmNlOc0Tllgm1>e&cnXYDM7mKPpE_1zII0+h-pSunEshd&!_F*aYNJ+sh9 zfc|gPS?WpK_fM-+r{l>*aYG+lEF#j3+3Z;tY#_N(q_3V$CJU2pyo{yoNDl@yf2#eA zaxhxX!Dxgge3aaZCdO7*4BvD6*G#*QQA|gX^Tu(RWr%tcV&BOq+-4`A?!52z{V`cf zG3Pz93vTi z4ekg@FwMtkV8Rbl%u+c+0i8wG2vkie@}z+x{h0a;_H#a^+dU+rTOFhpKXKS(d1L6K z#$yfg2=gRlUKgVd0ps_zL9j+}f|UidT8h*Q`$y2`nH22FDzK&oH5_PNv?THK;+u*C z--6;?cenOL85o)uv|5Jf6CYiEF#U7vF6z?c*)R&qvHrvoCgg7jZ#A&(E*k_I#ZOsJbouLs zW;XHF214g!0?lFvJLGCjEdKm|BC9!`GR-`KZ?L2je5gBt-4~#?^s@*!7TERZbw52OBIeJg-am zUnTCRWf~vIO+nPlklp)EGi|!{D<6hQaorN(MzHIJ9cNd}2$q_M-g48t`*k;WqlLT^ z*HVVikCyP+!1MAV;X5^B)=CGjv2?Q}8wM`X*Ow|bhGGm%;OVA(w0u|jdWe~@=~C)Z~?>)96d0rZo2DztYNGz&j{DoYq0N>eLKb zKFkg!8N%6Y32Z}iUFYyc@*<*}IiB{XJml*f>*XkN>juD;znYLISp7P;xM}PFaE3U$ z?8jW!_-1sZ%ku2hSUwIX+gZUmSSqk4wVBwZskkOc3xz^1fs+G*&;N&D`t&N9tsw}d zf*{?Y2|bxc#(ZG{{(t&^oKdY_3yPoeb+`r<5fiLAaAneL9+pqhlBOAUId>ZbgEU zZf3%sOIVW#;Iy@ovCO8b{{UUud|8-QN}i81v4pZ#2x-*sibPhicm98@R1wJA7PRPS zalsV!Gg$gJB4=D-n{}OVwHxqI>62fceCCeZ3Q6c`g9iMNxdUVe*2N!PiufV+3whI> zxU_I&wO?Z70yP?a`MY-3)Q3;&u^^6A^yxL>-b_>?X)uRr8T*p?r##i;R8++5PVlb7+1 zHB8vdlbeVk?&EPCHuTNh5evJ(_ytDQ#r> z+t-rs0Rze|j8OSHjVfHXSN?`WopE(%bMVo)dV%*baxBen{m;BT zwSP$Mv{vZ}(`4bzlayvb%Q8eI?BLsNOdS{FAG(OQzh;G${}^&T zq5NUBB_w6+w*G^o@eiv9MrJjf;}53;LD=E|K{3F}>p5jN37>M4l1dm8z2t2U6(ce3 zs>L)<@;Se<@aXu&@+YaIu9@qfK>~>ZwL+9_sYZ9wj)wmLay=`XuiFx~iyaD?wYs6A zAgvf)si=q@n^r1~+2@OjY}V+O{B$5}{>;PSM1Ui0&_J9GR5!QoZ(S_5sZOcNGLMkW z9haevvuu6~z-SuIi41HsV+=yxeIUiRe`(j`rxcBq-DSz5`Nm>khniSx1Bi0 zQ_yJ%?OlU-T{gaK*3mT;Lp5m6F$q%EH@T{-A2N|KTQYmze!^!F&$MKG?eltHDC(ug zhl3i2YE40bGTPrb2@zg+5->@Z zcAs*gRg!@-RvB}$xC}+H)gEWa0m$@*B#>t4=>j0@5=1?I@>`oP(J-)xekiL9 z+4P0R*lLxhn$OnM?IuoQNViO(7H75eNKszRIH%Ay0k=n<41(i2{gC@q1DOu3!idd- zC%Wl6Q0`R-crsMZaPBN&A=L%6C3e(+%G&~X#?j3YJtE4g`tC`8TAvH0X&$vluJD{X zrz$$^eNircS8&4y)X)zw^y8xxc>?5aF6jk&JGRh8+^6v5Rmq~D=cdrkS;`C2-jA+8 ztbMvm=DL`T6;m3(oW@+<|1fU|l3z&L%>XALA?mLX`>u+Kw#2y06EBw@LaSfthq9j_ z#_VfzAZ?AQmyh}z+G*5C>bmC=LHjzU*43sF_qk!zMH89xMk`)#t94m;OXjNA_Lu@Z z3ILeTs`224K7h)x7=EB$BgF^++j(T=I9`yh8@8~ofx>_UGN(;$U^b$}Q88SjkCEL5 zjU=p>xZ@E0knoZ|%^q4!0BFA&*<|rIr&WUj`jZiy-pQCUIrJ>i`c`jI5Fe z;-cb?W}Wo8ix)@^v-4x2H8fB0L$UR?!h#Yk$&+%0t9fJ3YQ;d2%dawBdJ zy4j-#MZbtdvTV#F*Z+cco&y0`!wsx>4Vf>C$ehI3@t5C`xj$$Z$t9qF*vf*F{VaOP zz1vxP&Z)S-v=~`bNan8ev_oyCz21sO#$NTjQ)lWH?@xmkU~m`Eo?jrT`(`*;O&`j=G1;+nv|jspyGskH zhSicKk0p5V3Bv>mF${{&%=m*dWxk6=gvGWL$<5d!DgF zmp*FgA|loH-PZ6fsR0jI^Z;m)XCToV-BLkx$+%l~%o5Uifo!p;us18sPUhP{EtxAB zqHFkOMdxUCO^(`7D9R&w%Ihv{%U>HrOup02ww^;!DwZ&Vv^ab@MTT-+zZ}4YF7Olt zsf=)SfTG8718$N96qW2$8dbxIsS6ffcm357sEWP-i4nakojMZ`>Z*Q@fF`VFQI6saQlu|E!5?V5uob$~$SZCGA$pY|$5?rXrxxZZVE%-SZAY zVONOzebf^{OUo1&s<%A$b1MK|A!>;Wm1AlQmS3H28sEJKz(h*1Hmtu~dHQ@xyVQuv zc&Dv8t}J8v<>@Bl*YAqpB-d24P8{4JsaE#uRXSz@S7o%mQt%2F*p6CfLA2zPpFl)6O(MJUC1{m}_6gmd>ifMdPwqLg=o9dqU1{W0n4F_Q zG6l`>q5|*eX+Ua|H~4ap90lymhmX+K=0N1U|FrPl zWY>=bVIgYuCLTT;435%*;m0k*gz?=T^xrP&L7RbqMnd?%M#Jx5MTu{?=@H1ZKA5&A-!mZ^Da{l5Q zuHFjosm1(Sr}MkQ-b5|lPmv%C6MAPhW!NbE{FWKivX6&}?byP?i2G&I{%<*g@rkr8 zRbGO0cd883LIYItl*+OeaA?-hSPc>_KWjBN5Y@P58JmuE@7Y*j&*{%`LJOyRDc1dQ z8Me@aeH00I2Y#gEWJh-*Jpv@^Ktz(nEjM7&=em@11k)B9{gyR%q^T5`FqUhul0H2b zw3{=LAq=)BRD>}hMEmSYcb%{c3uueSUd$;D0K)2dn<{XL=t}RHs4h!N^BTZ z9V$$iNqO%nBkaL(;V>4aN!KIB-pPexW7SIb5R~uR^=L^Qy>WeNgP+rPNVWMHSx~t5 zQ(eJ5J_~pcHK^RK%3bRQg+$*0dY0}K+O-~MpmKTRynh|C8Q!;HgTm2KA|nEmrBFzI z+mf;bj=sO~=y0<}huVgye*&`U^1CA8K%=SgMi8JM*jZ4}RaAcYZyqYAH^<4F)>AuG zi|PC%^Zl$6%HFAI{q3x=(Fnod9sC50pedp63*x#PC{g z{3z7OwP7>0F&5G7aTf%TAot>eTNY3Yd)}v9Hc$wx1p_26pYC!DS9&h=W$e|v%2Xig zqN)`NZ#!Yz=730}%+`1id6ZwG?b)`8#QlzYnDlR8IKtrDE(yQq4egjrZP*z4u{HOp z?nVi>528UBaql+M=JE@CIhDt()5%LG+{YN4jtS2ywXQ%D1fgq*izIS+cH8)QYIs)LAljOs z7X#*+I;QWC#j_Ou@UPWv)bH&dMFJLvl#@%@EuomFi}4+yWV#XFyA4oKtnRn+$H`{7 zq+`+z1LNqLxyT%65?-+;8x;dk0i5=?WpMy2o%6U4mX~rQc`njlnE&};>1G|P&~%HugqI8GPkIvWhw-r-3esw z>9fjLnitq28$1aHBng1ZxTTf({)X*R8xtr0*>R+$kz29L`SiCHQ>%My(HYZZ41cae zuRRoUf^zPGZw$*qAJ`b*s8Os>RvW#MQYK*v6)e6Az5(U8HXM}3Vhl(h4XS)#cUE0C zqN`7gJ3nh4I#QYX!|lG`({a|wDvf=NroUIyrvC5JOCnyG<~H3Xq`ZYbn> z->}U&XjO~co)u3#@cwSaaL<|Qet}}pMIYxK^}MUKR!n9w-wuBQK@KSUKA>Xd;-m1( zP@L?(VA10n@?y0K- zCUNe>SyUM(sDs`FGcevNq%G6*JP(fM1KTe>7re2g5=ro`a+=V|)aftiY9+Vtn&e4E zF}ySnXLBFOC;k=vSHjaM)LUr%q|ubu9iz3ax$8b5P3ip}W`u2S@G481(A0_u?Vlil z*)m+2RziRcG$0-)6+DZQeN$0%2lLw;M}SF|A#+WjtHW;Pm81uIv&Cq&AGdVHy=#c$ z1?sY<51P4$V5QL1)OvNC+<2}Id?a^(${x`m@H3NgpdG){-iT6}MSBc3DZCD&p_Ei|*!q$4lwKlt z>BGXd{h&4#MP!tZEBPj{#X2xm-0qg(Irt@ey*}wlR@=bbw=mMNvw=qqW(gPXl2-7Q z1QvjOKz5|7?QQW|Wyn#XwgIp2XOu^!ukN)wYj?DnJ(*aE z6%}q5Ur8A0hX#ttzncLp-At!+av;F6Zr<6vJPp@kZLS;YsBf(inTj;?hivb8$iV_pnmC3a1aZ?zw!%Gyt#(H%Qib z!e`6g-%$Wc!;vNwCAQzTMUB!Ow}^H3Foh;Kcz~}P<)}3P)+Ka*MTO-3-zp(ZeS_j@a=#w0H_xmK06B3f+Yq z=vmWe$d&?WF%H3`y>(H+=9ZYV)Xt(@AL;+<$urW_V{z@k71U`~b{e#M`*=G_tlb{Yzz`jWb$ty|UN@;S-e6Kjm#D^R*0h zr`Gk1-bib&4*>77`MIAu@$ARfj>wBXl?vBeJOox|xtW~+k{Ygx+usV{k0CP4*l`^X zE!ujg;6c^MfuTlyn{b&z9ZH6cU7FC!3ddk`Oxv?hJYfd#%7<^WHPyocuf)s$&B%Ud zhL09w*>+`?0YJA*Q^#oF!B zRUmP1`cQnK(aO&(MBS?k?-d@L)5A!JS9tXpyn!iF$k97zV$fjt>~p-!L{QLh)fg0Q<+38Mj3?&DPv{1x3fZJQE|Vl~n$w=^Nev}rFgzCH-G zScdS1CRm~T*me)s;)0TQ;Kq6+x+h0F(a&T?>+IrGY+*ch2*-`556eHZxC;Nsj45F8 zccx_6(Xq2jYmQU{?uuc7f+3a^_X`s%<5!@(=&HMt!WYDJy5-8abKh>;G~U9F>_nS9 zis0V{qYc@LdS(g9#lK3hG7e4k_I@QVKg?l+#1>x%iItNKOxy5*Szm=`<@=XD_^A!K zRrY6;h9pgC(<_jy^{kRxAjGlS4}cIygtM85MGe51s_OoXCc7EUabsMU%B;d?kQu$- z{SX~LbrD+a5KO7OEUoP3a_$x59!T$I4`>x>ykosCG_~H< zY=AOZ1d4PZ%!hcJea(3$=4x-7NbQ2|EnF{$I$hGQ^SY&!=}9Gi|6U6C#@F0fw$)L5H(mRU$~LVCWz8&;U?{f?po2mu+lg zL|iK=08Zr0RP(ofR$vfdos7xpe3_BG6Xdh8pFzF;*%1PdwDL1TpzIn2=!IV~VgefR ztcxrcdPG@=6{%%RDFeE!~wzjEW9k@=ri&hyKs*n4|89Q$= z@I*?5$RA(W_J`5nlo(HNdB@Iq%FmNN1yugnrZ{>~W(~W2KZvF?1tt9glnYii+@dO{ zEnL6f5wa#EY5$%sU;|M~AdHhyLfS5h+1LDUh0j~%lDRcd2>d0HD`r6YzIlx%2`uP_ zl*XjjL1b>8Y`_e)xWFx@=fnfg*`N&e*~j$6J+?q6Kv|?7v3G#hpCgGVjdOo&454Ig zAdRa7t0i+I(CWbW%WG|Bczt+bf8!%i7Cy;G8_s3_iKYRxPAnimsrZ=kX)Haiwe9)UfyI(J4JafSB2Cu2c>zwkJJ71Y2BS*6O6QA$Ws$id z@-6`9C9ElQuvWDhRKz;rBk&3I998NR0&c|@r1%J!pM8OhQ@;Em+(6{~mt@;-?=-&$Y`&L3+$=D#s5;Docmf?Q(#6M$GD!HsRk?9F}hWj|x2u}TJM zLf2-FlS3M`QQc)oKJY=upjA8OxO40^)(CJ9;HluZ`jW^F0AE=pRZghe<0ncHbD_3} zU0`0X+PSzg6ShTxrUfU%ht@iF+@j|qS>Wzdeul9?-7n*jChW4~qO4(;Akr^wr_<2D0dgwfPP)5Db~;FE46#l0+CvQj{;Tl; zuY`ODl*&Nitkqe&&#kyp5MDUAFqX@<@h3wZP#qSI?ItFm^3tZwnD+6$er@1eJ)?hl z{x^huTJ2g{;91o_tO>%YPnW+?MU8IlS~1cWcOEF~EISWV80Lc^#1S)Xa~12s{#Wja zpj`+;Mi!p|Nm)Pe8m$cQBOMwTs@Hx|k8CP2%E8~9Qol-=T7KEC{lUZuDi&m1;dWfL zS17ebEV{}`XMK(&avOm}O31KzT4WIH^%$6XKeVZ3!fHIn69@7(8`*qE)w_Y?vMAdY zCtI1C&q}1NU%;_>5`GDR0eg8w2J=Wzj3mxnk7Im%C!6j?Qh+U`!_&BL!)74nIrz@ zy&)GvTQFl0?w&ZzF9TY=5#^efhZ7(hWbH+$ckiv{TYjr-3%o$BTxjM-c!O*pnrw<0{-~kQnl3;;$%2Ohn2};nf=tRa)^uN?&xKOV9)?XKoon^7za?!D&;y-f4QA;W znd=~*1!7*;2P0TOTI_cGZlBrKL1x`Cb&-Szhc2)tc$iyz<=nu01~*@3QPBrALFX7d z(ib}sn&7DVC1f7F_wtsy+y}8y)z^Hs@QR}Cy%5#I8BFFG2$maVG)4QVgoDF}Kb-A2*?tyf@ue`N|O{ za4if8OzuFdeQisHnLyFnz|mW$_YZ-Z4NB_c7ME$?QW&j&2K`w+ncGjov-&DX5o?rm z08p?KZXa^4)2c9-+K|0AXjVPkV{PrVUAM)mLg!)8Y(b*?MK4`@hBQ+vT6AhOB;i4xF=^{g8JwmZ+R8*LkZo9_vzE8Hbr=&zk_I#lm zEF$WeN6z9BzbTSygxh1Tz#S^d<*kew@< z0B-EKk|?z5-4+0M`2tJ^SU2+=>Cw1GDU4LtJz1wV(|fe^k3?=TL$Qae;?geu_+yIP z0>%jXces8}e+lAb2LblPn$Mm-C7*LMaCQbQRK63}flc0vieJ3n3#LBq6}AcTaD;kv z9g06`+n1wlZUGgsM1+D2dcS4^Fb*FuK`c(gcPi|j2Gt%#4;R>$JM2 z$0uCUX~w1(XdbV8ZwwUm_LvPQQ)utN_Wv}b2ACV@6V7UH(G*)kxDAcFB2H6|6dI2$ zau*M9w6;-y&v~_>Uqluwj1*o+>(+{OtFGDg5PlfM+yJp+c-kNXBa>Cb(&E!qk+0k6 z#kS7o`!Q#-1q?dOpv@X^l}jqpI+Y6XM^)0_zE2k-YsbwqwH|`B=0Q6z%P?1rLdDiN zZjjTTf|_bOo1AZQr6}V_hZC4<2{Q#*gW4F&(@HClZSU|2_f_^5>cCvAU`i3y%)V_7 zx<#EUvEsmy+fUc~fWL>QAZ=-%g6gcoTKWt9a& zi^V9oLn-BmLdzuR>#l0haKJa)#kMn3!B+?}6`M|+b<8LYAkhzbtsJ6(>?wA(mfHAp zRvK&3{yt3zSBBPPj#@=CFC!@t_YOwB1_1K`CY^~7vL_F1uAc%EiPzv2<=LJbi8^)4 z%%ibn)|(!!6BwFNc{Wzb7IbVc%VF|D;*LoQip@^N*Ol`kOCLNn_K7&e*@(W}{sF=(}Y( zP~M>Z+!ET!2*@F?Sd!V8cHkB!=6@ZOrV97#V(P$rSZAAy#gqW{Is1kf;~D)>@8im0 zE90|}7pCqM^IjL2>9zzTR30L48DF1%UKw35?gc>IkHrs+ItNJ5ns8kcqJQ^2oJP>X z4Jl6I+yZW%Ip9*E&EI^&9qxtt+%#*lF%VJ{;)V8(Rc3`&IcW!gNOJ|#D0;n*!;ef^ zz322lPZPzxp67Y|T@OrNv5{{zR2y!qFc7et6V=03vS-b~ZX_B1g578!bKp9elKSGl z3!j)^19VQ9n(-Wr4siz04wHRihw3apelZ9i}}_Sz?Z_F z2fT=PI_RhSkTa4swRplnzua@giXh!wD5nfBpc*THvL2;9q8}1!Dq?H-&hy5f7?8~j zQGC#904)S`;lEhpctYFSzUT$A(3;4(^`HrSv}6uI=-zWx!&U?X2QM)7l)ec3ZZQJx z2o?mCFvHc+>t!t4QB1dsHgOU8%7|ZMw@-EX40`SEI8fqi9~gF%jurx0(tgiO0B*|) zWtCyugJE9a7(ry=25k?gMl`saLd9Cy3?zF-|nFxy~n?=vi#Q9sgAfzp-3V)N=b}vE=;Qb&%QH-VDf<{1cZwx5wfD z+UAFetQVBjOtnwK)1T0{+Q1kq;ay_M+}W|?Pa&tu%CNN8{d$4gw2MGffV*@DjkC(D z16#4{!1i_(s2QPA=}&`{)#%HnbFgX9A^`&^f5y8Kxq_!>%zMfcozHzffYNC)e3_=?ZjX+Zd7POv>iXIpJ$LV#vc)djDL*yXCg*G~KYCcfjzpBI3O^TXF_9Wk^ zuYN&kX9PV}Ita4&8vjBLN?;1OM~lichg#@71-~pHe#PXYk*_)PK=6XGS0TIont8il zBj>AxDS;1k!3kY(Slsd@S~a<}VdVvdH*ROt#a@UvkrZ(4x!Fl-D0w1u)^V6Hrwzs& z&}?ugqy!cZwfe zhu*;^l9q&4vNz2dj|?}eQ3l9ysi(_(b+Q+}#Qa6%i8ty`W@ZEiKZR)T=!fPfJ;~O% z-N34)I_xF56pV_4QJZRw?u+&4eJB)yu3#8vVr5w8xr?3q#^&&zP+%cQK zpb#F)krLl^Cz0yM7|$C<&(;>fr5ZK@tH{0=`hos%X+0YWf znfnJ^XC2IMZ43JlReXQiEeW5yXHC`u;PA`pbA`Id!<)p4ii~R1h7_PFVuSD zZP+xsNEZu#xmAAjD>EQrG+2L^3>y)UfE65@Y9a%5MsNd zv0}JG5j5kM_=WSWUOMA_!G?L+#6Yt$EDFj6$nLBG6m^U_dgM5PS!aS1 z0|>HI$hp0Ii!R;a$RB_5Vob*LQ%p>rO9%am!Nch^9FO602n|hBBl~3GWfJfL?QIiq z+K9{Az;GfdyTY-x%@#X~!?HCWSm+V?%IdgU|K@Wp~f#Va}ROhtBk56K)> z{?JU0HiSgArW6WE%&g7DaTd^AI0qcRdu%uBgvk6>2rSGB3b^^2q3+S?X)ih(7~ckq z^_Lsa`5)bXT!3Ef|N1=bBHR?mt3Ypxjt1lLy#i^k{2qko7B5L}a&gDpdY+JCZ3uDCyq_qTT-dTU6#<2`2BPKFqC*qBHA8c@8_rk! z3QSVq97k&gRgL1zrwzGDcNDR1cu)(Q5=j0r_u3MQ0(tHOa6_yGz*>7#C;_-(Kk%O< zu(oh%EH5kX_paVkWHof*s>FO4;cdmz=qItytTj0SYn5t8R3obtN}1PV@c2rFs2~8T zex0Pv|D4@*gmyfGI1a>8`zKA(%5I-zH9v%kpUWMyO*Z)@} zzjr;Zdnq(LbV>V2ZF}3xPNU*C{zn(Us9~5ZlnZ=y{WD~fj}Z?L)e|_lTj2gvSA~(D zr9&V(NoSo+XW=bJ*1^qqB{6{O75Ra;k`ZW^BWtnRe+1Ajks=tHyckM=jhHEE;B$o8 zlru-Ea1O23#X3#AG6CZ)N*97G2n*w)E>WGAi{CKLQbmDrc9$R>AS4YHBGD|3wHlli zic`ZMEa5aGsEK=+v$jGbQD^=dxv6BXA9$lS@8r9r6p1=>;=NUcO%N-5;uLb8GxEt1 z%q6C{(YlSH8lX_abiiuR0FFEsYhDj-!;GUBETD;bo)eziGf#(yIaH5Rjp}dM=6_5V zLU){hy}XA^Irp!m(mjl$Wj2$SnboRM@=}R9PMN7-8k*fsge01_ zGrVM}T`;>KAsSeEI^j7PdfJqxmYxaGahD>79`Dl7$U#a^rYVIC4JM;DYM7xycwX1} zGiI&*>$~^6_S)~eKkxJXK2H-K;Fq*aLs|yo^hXFiw z-hZF?q4!}4%T1cjdjd?Wr5EXuX=y$GIdh^AfKdakewj8;f&LCaZ~G3t{^YW(E}n)r zEX()Q8e}%d_-_2{mR|&tPH65EvdCaM5c5yk-Q*GukulDKmqloa%WgyQk4vixsJ$D# z2X9OIO8r$f!;OG}!v|A>dQO6)plgQi+^ z0sKu^{v2*1i|b}gQ%<+Eja!M)pvM@MVR`(k_(3OvLbq2_V759lImwx<3vius{qd6jFRr$n9h5ULIhrsow{6D5GaEFMj@e zKKCut`dFE7F?vU;lQC5zLBf((QTfmyT9Y&85Qi)DA^OKazgm7?S-yWMYWMkOAO|9; zfYpE}tn%)uN>Ff$z&X)tL|NKl-~@K$YDo&9PoUa`zI%1&DDayj5HeAb>zsRSrAd@x z+GYVFWDY~sz=8!|_I75OW#L+_nTwq#=y;N)6_w8 z;OMA65~n!#x+3IgP#fVD7EVeQSB%TRRH{xszmB9bq|N=H82w&CM{~@D9A9_pfQRus zz|PMrr`CX0j8|Wa(3xv5>5wZ)P*B+=-v{*?E!7IWN96m)$QBE_Q~14cU`|1 z|E{)Y(>U7fCZxCIT_%iWA``RF+&7x?%=;#ByhXcRb2JoE3(8{72csFA>rZpE3$25P zus{_-CffX6=pEdqfqR*gx4J2|PJVn*WdJYvru~Tw{7l~jZYz;4M98%WVMW{a=N`?V zRcdpfb)e;N9b)$rGTXX>y69olcCZdevs;i(EErsuq#brFXnYC+Us^Frn?T!PYR^9B z1SpT3Wkbcn?cZij+{s(`VMYew6p3M%y@{{48!e5#5W{p!c4s{q_Rh)9@wBwJ3K$Q^ z?$~Cr3)Q}Mo((08+$Y!q8SQb>B0(`tfu^|p5>mqs=>b_;c-BvljnTg4;`1nX<(5~B zSeV+S1VI&}-NeovzYGnQV%jnit2t50H~VJan1}`jDc?ix4LkjF^qj45JgeSAF5?C7 zk{6Bi6?bovld9Oib_k8=#s+sKUVb;aT_z;!>!&8;R}+u#6nKv7F|W$4Hglt3mIoz7 zF!cKA$WZm3t+>h?yc8XV;J$EJI>|~SlK}q3F++XO6__sG=-<55tT;v@|IYaO4L%<_ zJ8#^sgiT-kLiZG7vUVF9i4WjlKlwU^>CF|2fib@HENWluF=Bu`GSanll;?Q$G39b- zgx-vah-k<*(Es&Ou^m4*aH`Y3FFZYO>KxA;1Lt|Rq?Jt1GA}0%{Ve~l*yZ-gub+3f zZj^_#35n?I7z0WF=u~DxEm_}=;j15=5dh1uI0>T+mS@B5fpQ&es(LrqrP@Z;-?kxL zG;fp-?`RoX?XL8bTNjO&2fNF?!F^hrFwBT_wVz50vPFWgR#lW^Sf|etiYG#7Hy?hA zgH@ZPdEJ7=t1yd^m}l++89!T&a(vS559#$7PtOI4)cBOM6E9UexrN?@pz7%Ue{A!K4l(J3VapY z8GrC7OOA)nV3a=QT-RFgs8$Xp&Qlp8=lS1pmU#yGKY1`nBXZuOro;3PF@+?39_@O> zi{0>fzdLKctJ8hCk_9uDO*L)ePWVI7XtZOewXl#5o5DG-2)f?*2rv0|D;iUy25BxC zHnM>8t4t1oYv(;Kv7s+wuPQ7PR@r4@>(>Cq{HWwEPiDxC&hfEKtU)fnADAPh?|Ks2 zKj#HJkdB33RJW9S--?{;S4dfAu24(kPtIRF=TH!tFr@95cj-`PEOf?y?;G(jn6h&_ za{tJEi|VqDe>M~HTbgFAr25f>zUp580b)9x!+&b9ydb+&ana&_-=&HQ3w9;a`SR}J j<*=1M+13C17yDHGy8u-5j~O6z4n_P6d4JvB6My|5=x@p2 From 15fd357267198edd23036785eb38a43793bc5e54 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 31 Jul 2023 18:08:54 +0300 Subject: [PATCH 393/446] update labels --- openpype/hosts/houdini/plugins/create/create_bgeo.py | 2 +- openpype/hosts/houdini/plugins/create/create_pointcache.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index a1101fd045..a3f31e7e94 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -8,7 +8,7 @@ from openpype.lib import EnumDef class CreateBGEO(plugin.HoudiniCreator): """BGEO pointcache creator.""" identifier = "io.openpype.creators.houdini.bgeo" - label = "BGEO PointCache" + label = "PointCache (Bgeo)" family = "pointcache" icon = "gears" diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 554d5f2016..7eaf2aff2b 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -8,7 +8,7 @@ import hou class CreatePointCache(plugin.HoudiniCreator): """Alembic ROP to pointcache""" identifier = "io.openpype.creators.houdini.pointcache" - label = "Point Cache" + label = "PointCache (Abc)" family = "pointcache" icon = "gears" From 65c9582d5513afd4e16e6baa60550a00f543b5c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 31 Jul 2023 18:28:39 +0200 Subject: [PATCH 394/446] Nuke: farm rendering of prerender ignore roots in nuke (#5366) * OP-6407 - fix wrong value used in comparison `prerender.farm` is correct value for prerender family sent to farm * OP-6407 - added test class for prerender family --- openpype/pipeline/farm/pyblish_functions.py | 2 +- ...test_deadline_publish_in_nuke_prerender.py | 106 ++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/integration/hosts/nuke/test_deadline_publish_in_nuke_prerender.py diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 2df8269d79..e979c2d6ae 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -212,7 +212,7 @@ def create_skeleton_instance( "This may cause issues.").format(source)) family = ("render" - if "prerender" not in instance.data["families"] + if "prerender.farm" not in instance.data["families"] else "prerender") families = [family] diff --git a/tests/integration/hosts/nuke/test_deadline_publish_in_nuke_prerender.py b/tests/integration/hosts/nuke/test_deadline_publish_in_nuke_prerender.py new file mode 100644 index 0000000000..57e2f78973 --- /dev/null +++ b/tests/integration/hosts/nuke/test_deadline_publish_in_nuke_prerender.py @@ -0,0 +1,106 @@ +import logging + +from tests.lib.assert_classes import DBAssert +from tests.integration.hosts.nuke.lib import NukeDeadlinePublishTestClass + +log = logging.getLogger("test_publish_in_nuke") + + +class TestDeadlinePublishInNukePrerender(NukeDeadlinePublishTestClass): + """Basic test case for publishing in Nuke and Deadline for prerender + + It is different from `test_deadline_publish_in_nuke` as that one is for + `render` family >> this test expects different subset names. + + Uses generic TestCase to prepare fixtures for test data, testing DBs, + env vars. + + !!! + It expects path in WriteNode starting with 'c:/projects', it replaces + it with correct value in temp folder. + Access file path by selecting WriteNode group, CTRL+Enter, update file + input + !!! + + Opens Nuke, run publish on prepared workile. + + Then checks content of DB (if subset, version, representations were + created. + Checks tmp folder if all expected files were published. + + How to run: + (in cmd with activated {OPENPYPE_ROOT}/.venv) + {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py + runtests ../tests/integration/hosts/nuke # noqa: E501 + + To check log/errors from launched app's publish process keep PERSIST + to True and check `test_openpype.logs` collection. + """ + TEST_FILES = [ + ("1aQaKo3cF-fvbTfvODIRFMxgherjbJ4Ql", + "test_nuke_deadline_publish_in_nuke_prerender.zip", "") + ] + + APP_GROUP = "nuke" + + TIMEOUT = 180 # publish timeout + + # could be overwritten by command line arguments + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" + PERSIST = False # True - keep test_db, test_openpype, outputted test files + TEST_DATA_FOLDER = None + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + failures = [] + + failures.append(DBAssert.count_of_types(dbcon, "version", 2)) + + failures.append( + DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1})) + + # prerender has only default subset format `{family}{variant}`, + # Key01 is used variant + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="prerenderKey01")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="workfileTest_task")) + + failures.append( + DBAssert.count_of_types(dbcon, "representation", 2)) + + additional_args = {"context.subset": "workfileTest_task", + "context.ext": "nk"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "prerenderKey01", + "context.ext": "exr"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + # prerender doesn't have set creation of review by default + additional_args = {"context.subset": "prerenderKey01", + "name": "thumbnail"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + additional_args = {"context.subset": "prerenderKey01", + "name": "h264_mov"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + assert not any(failures) + + +if __name__ == "__main__": + test_case = TestDeadlinePublishInNukePrerender() From 28768dc01216a56bc48a9b2659082e4893081bae Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:37:46 +0200 Subject: [PATCH 395/446] AYON: Environment variables and functions (#5361) * use proper start script * implemented ayon variants of execute functions * more suitable names of functions * use 'PACKAGE_DIR' instead of 'OPENPYPE_REPOS_ROOT' environment variable * use suitable enviornment variables in ayon mode * keep sync server in openpype * Better comment --- openpype/cli.py | 49 ++++++++- openpype/lib/__init__.py | 8 +- openpype/lib/applications.py | 20 +++- openpype/lib/execute.py | 158 ++++++++++++++++++++++++----- openpype/lib/openpype_version.py | 25 ++++- server_addon/create_ayon_addons.py | 3 +- 6 files changed, 225 insertions(+), 38 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index bc837cdeba..6d6a34b0fb 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -5,6 +5,7 @@ import sys import code import click +from openpype import AYON_SERVER_ENABLED from .pype_commands import PypeCommands @@ -46,7 +47,11 @@ def main(ctx): if ctx.invoked_subcommand is None: # Print help if headless mode is used - if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": + if AYON_SERVER_ENABLED: + is_headless = os.getenv("AYON_HEADLESS_MODE") == "1" + else: + is_headless = os.getenv("OPENPYPE_HEADLESS_MODE") == "1" + if is_headless: print(ctx.get_help()) sys.exit(0) else: @@ -57,6 +62,9 @@ def main(ctx): @click.option("-d", "--dev", is_flag=True, help="Settings in Dev mode") def settings(dev): """Show Pype Settings UI.""" + + if AYON_SERVER_ENABLED: + raise RuntimeError("AYON does not support 'settings' command.") PypeCommands().launch_settings_gui(dev) @@ -110,6 +118,8 @@ def eventserver(ftrack_url, on linux and window service). """ + if AYON_SERVER_ENABLED: + raise RuntimeError("AYON does not support 'eventserver' command.") PypeCommands().launch_eventservercli( ftrack_url, ftrack_user, @@ -134,6 +144,10 @@ def webpublisherwebserver(executable, upload_dir, host=None, port=None): Expect "pype.club" user created on Ftrack. """ + if AYON_SERVER_ENABLED: + raise RuntimeError( + "AYON does not support 'webpublisherwebserver' command." + ) PypeCommands().launch_webpublisher_webservercli( upload_dir=upload_dir, executable=executable, @@ -196,6 +210,10 @@ def remotepublishfromapp(project, path, host, user=None, targets=None): More than one path is allowed. """ + if AYON_SERVER_ENABLED: + raise RuntimeError( + "AYON does not support 'remotepublishfromapp' command." + ) PypeCommands.remotepublishfromapp( project, path, host, user, targets=targets ) @@ -214,11 +232,15 @@ def remotepublish(project, path, user=None, targets=None): More than one path is allowed. """ + if AYON_SERVER_ENABLED: + raise RuntimeError("AYON does not support 'remotepublish' command.") PypeCommands.remotepublish(project, path, user, targets=targets) @main.command(context_settings={"ignore_unknown_options": True}) def projectmanager(): + if AYON_SERVER_ENABLED: + raise RuntimeError("AYON does not support 'projectmanager' command.") PypeCommands().launch_project_manager() @@ -335,6 +357,8 @@ def syncserver(active_site): var OPENPYPE_LOCAL_ID set to 'active_site'. """ + if AYON_SERVER_ENABLED: + raise RuntimeError("AYON does not support 'syncserver' command.") PypeCommands().syncserver(active_site) @@ -347,6 +371,8 @@ def repack_version(directory): recalculating file checksums. It will try to use version detected in directory name. """ + if AYON_SERVER_ENABLED: + raise RuntimeError("AYON does not support 'repack-version' command.") PypeCommands().repack_version(directory) @@ -358,6 +384,9 @@ def repack_version(directory): "--dbonly", help="Store only Database data", default=False, is_flag=True) def pack_project(project, dirpath, dbonly): """Create a package of project with all files and database dump.""" + + if AYON_SERVER_ENABLED: + raise RuntimeError("AYON does not support 'pack-project' command.") PypeCommands().pack_project(project, dirpath, dbonly) @@ -370,6 +399,8 @@ def pack_project(project, dirpath, dbonly): "--dbonly", help="Store only Database data", default=False, is_flag=True) def unpack_project(zipfile, root, dbonly): """Create a package of project with all files and database dump.""" + if AYON_SERVER_ENABLED: + raise RuntimeError("AYON does not support 'unpack-project' command.") PypeCommands().unpack_project(zipfile, root, dbonly) @@ -384,9 +415,17 @@ def interactive(): Executable 'openpype_gui' on Windows won't work. """ - from openpype.version import __version__ + if AYON_SERVER_ENABLED: + version = os.environ["AYON_VERSION"] + banner = ( + f"AYON launcher {version}\nPython {sys.version} on {sys.platform}" + ) + else: + from openpype.version import __version__ - banner = f"OpenPype {__version__}\nPython {sys.version} on {sys.platform}" + banner = ( + f"OpenPype {__version__}\nPython {sys.version} on {sys.platform}" + ) code.interact(banner) @@ -395,11 +434,13 @@ def interactive(): is_flag=True, default=False) def version(build): """Print OpenPype version.""" + if AYON_SERVER_ENABLED: + print(os.environ["AYON_VERSION"]) + return from openpype.version import __version__ from igniter.bootstrap_repos import BootstrapRepos, OpenPypeVersion from pathlib import Path - import os if getattr(sys, 'frozen', False): local_version = BootstrapRepos.get_version( diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 40df264452..f1eb564e5e 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -5,11 +5,11 @@ import sys import os import site +from openpype import PACKAGE_DIR # Add Python version specific vendor folder python_version_dir = os.path.join( - os.getenv("OPENPYPE_REPOS_ROOT", ""), - "openpype", "vendor", "python", "python_{}".format(sys.version[0]) + PACKAGE_DIR, "vendor", "python", "python_{}".format(sys.version[0]) ) # Prepend path in sys paths sys.path.insert(0, python_version_dir) @@ -55,11 +55,13 @@ from .env_tools import ( from .terminal import Terminal from .execute import ( + get_ayon_launcher_args, get_openpype_execute_args, get_linux_launcher_args, execute, run_subprocess, run_detached_process, + run_ayon_launcher_process, run_openpype_process, clean_envs_for_openpype_process, path_to_subprocess_arg, @@ -175,11 +177,13 @@ __all__ = [ "emit_event", "register_event_callback", + "get_ayon_launcher_args", "get_openpype_execute_args", "get_linux_launcher_args", "execute", "run_subprocess", "run_detached_process", + "run_ayon_launcher_process", "run_openpype_process", "clean_envs_for_openpype_process", "path_to_subprocess_arg", diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index fbde59ced5..fac3e33f71 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -11,6 +11,7 @@ from abc import ABCMeta, abstractmethod import six +from openpype import AYON_SERVER_ENABLED, PACKAGE_DIR from openpype.client import ( get_project, get_asset_by_name, @@ -1435,10 +1436,8 @@ def _add_python_version_paths(app, env, logger, modules_manager): return # Add Python 2/3 modules - openpype_root = os.getenv("OPENPYPE_REPOS_ROOT") python_vendor_dir = os.path.join( - openpype_root, - "openpype", + PACKAGE_DIR, "vendor", "python" ) @@ -1959,17 +1958,28 @@ def get_non_python_host_kwargs(kwargs, allow_console=True): allow_console (bool): use False for inner Popen opening app itself or it will open additional console (at least for Harmony) """ + if kwargs is None: kwargs = {} if platform.system().lower() != "windows": return kwargs - executable_path = os.environ.get("OPENPYPE_EXECUTABLE") + if AYON_SERVER_ENABLED: + executable_path = os.environ.get("AYON_EXECUTABLE") + else: + executable_path = os.environ.get("OPENPYPE_EXECUTABLE") + executable_filename = "" if executable_path: executable_filename = os.path.basename(executable_path) - if "openpype_gui" in executable_filename: + + if AYON_SERVER_ENABLED: + is_gui_executable = "ayon_console" not in executable_filename + else: + is_gui_executable = "openpype_gui" in executable_filename + + if is_gui_executable: kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, "stdout": subprocess.DEVNULL, diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index b3c8185d3e..c54541a116 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -164,12 +164,19 @@ def run_subprocess(*args, **kwargs): return full_output -def clean_envs_for_openpype_process(env=None): - """Modify environments that may affect OpenPype process. +def clean_envs_for_ayon_process(env=None): + """Modify environments that may affect ayon-launcher process. Main reason to implement this function is to pop PYTHONPATH which may be affected by in-host environments. + + Args: + env (Optional[dict[str, str]]): Environment variables to modify. + + Returns: + dict[str, str]: Environment variables for ayon process. """ + if env is None: env = os.environ @@ -181,6 +188,64 @@ def clean_envs_for_openpype_process(env=None): return env +def clean_envs_for_openpype_process(env=None): + """Modify environments that may affect OpenPype process. + + Main reason to implement this function is to pop PYTHONPATH which may be + affected by in-host environments. + """ + + if AYON_SERVER_ENABLED: + return clean_envs_for_ayon_process(env=env) + + if env is None: + env = os.environ + + # Exclude some environment variables from a copy of the environment + env = env.copy() + for key in ["PYTHONPATH", "PYTHONHOME"]: + env.pop(key, None) + + return env + + +def run_ayon_launcher_process(*args, **kwargs): + """Execute OpenPype process with passed arguments and wait. + + Wrapper for 'run_process' which prepends OpenPype executable arguments + before passed arguments and define environments if are not passed. + + Values from 'os.environ' are used for environments if are not passed. + They are cleaned using 'clean_envs_for_openpype_process' function. + + Example: + ``` + run_ayon_process("run", "") + ``` + + Args: + *args (str): ayon-launcher cli arguments. + **kwargs (Any): Keyword arguments for subprocess.Popen. + + Returns: + str: Full output of subprocess concatenated stdout and stderr. + """ + + args = get_ayon_launcher_args(*args) + env = kwargs.pop("env", None) + # Keep env untouched if are passed and not empty + if not env: + # Skip envs that can affect OpenPype process + # - fill more if you find more + env = clean_envs_for_openpype_process(os.environ) + + # Only keep OpenPype version if we are running from build. + if not is_running_from_build(): + env.pop("OPENPYPE_VERSION", None) + + return run_subprocess(args, env=env, **kwargs) + + def run_openpype_process(*args, **kwargs): """Execute OpenPype process with passed arguments and wait. @@ -191,14 +256,16 @@ def run_openpype_process(*args, **kwargs): They are cleaned using 'clean_envs_for_openpype_process' function. Example: - ``` - run_detached_process("run", "") - ``` + >>> run_openpype_process("version") Args: *args (tuple): OpenPype cli arguments. - **kwargs (dict): Keyword arguments for for subprocess.Popen. + **kwargs (dict): Keyword arguments for subprocess.Popen. """ + + if AYON_SERVER_ENABLED: + return run_ayon_launcher_process(*args, **kwargs) + args = get_openpype_execute_args(*args) env = kwargs.pop("env", None) # Keep env untouched if are passed and not empty @@ -221,18 +288,18 @@ def run_detached_process(args, **kwargs): They are cleaned using 'clean_envs_for_openpype_process' function. Example: - ``` - run_detached_openpype_process("run", "") - ``` + >>> run_detached_process("run", "./path_to.py") + Args: *args (tuple): OpenPype cli arguments. - **kwargs (dict): Keyword arguments for for subprocess.Popen. + **kwargs (dict): Keyword arguments for subprocess.Popen. Returns: subprocess.Popen: Pointer to launched process but it is possible that launched process is already killed (on linux). """ + env = kwargs.pop("env", None) # Keep env untouched if are passed and not empty if not env: @@ -296,6 +363,39 @@ def path_to_subprocess_arg(path): return subprocess.list2cmdline([path]) +def get_ayon_launcher_args(*args): + """Arguments to run ayon-launcher process. + + Arguments for subprocess when need to spawn new pype process. Which may be + needed when new python process for pype scripts must be executed in build + pype. + + Reasons: + Ayon-launcher started from code has different executable set to + virtual env python and must have path to script as first argument + which is not needed for built application. + + Args: + *args (str): Any arguments that will be added after executables. + + Returns: + list[str]: List of arguments to run ayon-launcher process. + """ + + executable = os.environ["AYON_EXECUTABLE"] + launch_args = [executable] + + executable_filename = os.path.basename(executable) + if "python" in executable_filename.lower(): + filepath = os.path.join(os.environ["AYON_ROOT"], "start.py") + launch_args.append(filepath) + + if args: + launch_args.extend(args) + + return launch_args + + def get_openpype_execute_args(*args): """Arguments to run pype command. @@ -311,17 +411,17 @@ def get_openpype_execute_args(*args): It is possible to pass any arguments that will be added after pype executables. """ + + if AYON_SERVER_ENABLED: + return get_ayon_launcher_args(*args) + executable = os.environ["OPENPYPE_EXECUTABLE"] launch_args = [executable] executable_filename = os.path.basename(executable) if "python" in executable_filename.lower(): - filename = "start.py" - if AYON_SERVER_ENABLED: - filename = "ayon_start.py" - launch_args.append( - os.path.join(os.environ["OPENPYPE_ROOT"], filename) - ) + filepath = os.path.join(os.environ["OPENPYPE_ROOT"], "start.py") + launch_args.append(filepath) if args: launch_args.extend(args) @@ -338,6 +438,9 @@ def get_linux_launcher_args(*args): It is possible that this function is used in OpenPype build which does not have yet the new executable. In that case 'None' is returned. + Todos: + Replace by script in scripts for ayon-launcher. + Args: args (iterable): List of additional arguments added after executable argument. @@ -346,19 +449,24 @@ def get_linux_launcher_args(*args): list: Executables with possible positional argument to script when called from code. """ - filename = "app_launcher" - openpype_executable = os.environ["OPENPYPE_EXECUTABLE"] - executable_filename = os.path.basename(openpype_executable) + filename = "app_launcher" + if AYON_SERVER_ENABLED: + executable = os.environ["AYON_EXECUTABLE"] + else: + executable = os.environ["OPENPYPE_EXECUTABLE"] + + executable_filename = os.path.basename(executable) if "python" in executable_filename.lower(): - script_path = os.path.join( - os.environ["OPENPYPE_ROOT"], - "{}.py".format(filename) - ) - launch_args = [openpype_executable, script_path] + if AYON_SERVER_ENABLED: + root = os.environ["AYON_ROOT"] + else: + root = os.environ["OPENPYPE_ROOT"] + script_path = os.path.join(root, "{}.py".format(filename)) + launch_args = [executable, script_path] else: new_executable = os.path.join( - os.path.dirname(openpype_executable), + os.path.dirname(executable), filename ) executable_path = find_executable(new_executable) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index bdf7099f61..1c8356d5fe 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -26,8 +26,25 @@ def get_openpype_version(): return openpype.version.__version__ +def get_ayon_launcher_version(): + version_filepath = os.path.join( + os.environ["AYON_ROOT"], + "version.py" + ) + if not os.path.exists(version_filepath): + return None + content = {} + with open(version_filepath, "r") as stream: + exec(stream.read(), content) + return content["__version__"] + + def get_build_version(): """OpenPype version of build.""" + + if AYON_SERVER_ENABLED: + return get_ayon_launcher_version() + # Return OpenPype version if is running from code if not is_running_from_build(): return get_openpype_version() @@ -51,7 +68,11 @@ def is_running_from_build(): Returns: bool: True if running from build. """ - executable_path = os.environ["OPENPYPE_EXECUTABLE"] + + if AYON_SERVER_ENABLED: + executable_path = os.environ["AYON_EXECUTABLE"] + else: + executable_path = os.environ["OPENPYPE_EXECUTABLE"] executable_filename = os.path.basename(executable_path) if "python" in executable_filename.lower(): return False @@ -59,6 +80,8 @@ def is_running_from_build(): def is_staging_enabled(): + if AYON_SERVER_ENABLED: + return os.getenv("AYON_USE_STAGING") == "1" return os.environ.get("OPENPYPE_USE_STAGING") == "1" diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index 61dbd5c8d9..8be9baa983 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -203,7 +203,8 @@ def create_openpype_package( ignored_modules = [ "ftrack", "shotgrid", - "sync_server", + # Sync server is still expected at multiple places + # "sync_server", "example_addons", "slack" ] From 6196ded1a9cfb17154a3a35b735f25ac41690721 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Aug 2023 10:46:59 +0200 Subject: [PATCH 396/446] Chore: Python 2 support fix (#5375) * remove f-string formatting * added python 2 compatible click into python 2 vendor --- openpype/hosts/maya/api/plugin.py | 2 +- .../vendor/python/python_2/click/__init__.py | 79 + .../python/python_2/click/_bashcomplete.py | 375 +++ .../vendor/python/python_2/click/_compat.py | 786 +++++++ .../python/python_2/click/_termui_impl.py | 657 ++++++ .../vendor/python/python_2/click/_textwrap.py | 37 + .../python/python_2/click/_unicodefun.py | 131 ++ .../python/python_2/click/_winconsole.py | 370 +++ openpype/vendor/python/python_2/click/core.py | 2030 +++++++++++++++++ .../python/python_2/click/decorators.py | 333 +++ .../python/python_2/click/exceptions.py | 253 ++ .../python/python_2/click/formatting.py | 283 +++ .../vendor/python/python_2/click/globals.py | 47 + .../vendor/python/python_2/click/parser.py | 428 ++++ .../vendor/python/python_2/click/termui.py | 681 ++++++ .../vendor/python/python_2/click/testing.py | 382 ++++ .../vendor/python/python_2/click/types.py | 762 +++++++ .../vendor/python/python_2/click/utils.py | 455 ++++ 18 files changed, 8090 insertions(+), 1 deletion(-) create mode 100644 openpype/vendor/python/python_2/click/__init__.py create mode 100644 openpype/vendor/python/python_2/click/_bashcomplete.py create mode 100644 openpype/vendor/python/python_2/click/_compat.py create mode 100644 openpype/vendor/python/python_2/click/_termui_impl.py create mode 100644 openpype/vendor/python/python_2/click/_textwrap.py create mode 100644 openpype/vendor/python/python_2/click/_unicodefun.py create mode 100644 openpype/vendor/python/python_2/click/_winconsole.py create mode 100644 openpype/vendor/python/python_2/click/core.py create mode 100644 openpype/vendor/python/python_2/click/decorators.py create mode 100644 openpype/vendor/python/python_2/click/exceptions.py create mode 100644 openpype/vendor/python/python_2/click/formatting.py create mode 100644 openpype/vendor/python/python_2/click/globals.py create mode 100644 openpype/vendor/python/python_2/click/parser.py create mode 100644 openpype/vendor/python/python_2/click/termui.py create mode 100644 openpype/vendor/python/python_2/click/testing.py create mode 100644 openpype/vendor/python/python_2/click/types.py create mode 100644 openpype/vendor/python/python_2/click/utils.py diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 34b61698a3..0ee02d8485 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -439,7 +439,7 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): creator_identifier = cmds.getAttr(node + ".creator_identifier") if creator_identifier == self.identifier: - self.log.info(f"Found node: {node}") + self.log.info("Found node: {}".format(node)) return node def _create_layer_instance_node(self, layer): diff --git a/openpype/vendor/python/python_2/click/__init__.py b/openpype/vendor/python/python_2/click/__init__.py new file mode 100644 index 0000000000..2b6008f2dd --- /dev/null +++ b/openpype/vendor/python/python_2/click/__init__.py @@ -0,0 +1,79 @@ +""" +Click is a simple Python module inspired by the stdlib optparse to make +writing command line scripts fun. Unlike other modules, it's based +around a simple API that does not come with too much magic and is +composable. +""" +from .core import Argument +from .core import BaseCommand +from .core import Command +from .core import CommandCollection +from .core import Context +from .core import Group +from .core import MultiCommand +from .core import Option +from .core import Parameter +from .decorators import argument +from .decorators import command +from .decorators import confirmation_option +from .decorators import group +from .decorators import help_option +from .decorators import make_pass_decorator +from .decorators import option +from .decorators import pass_context +from .decorators import pass_obj +from .decorators import password_option +from .decorators import version_option +from .exceptions import Abort +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import FileError +from .exceptions import MissingParameter +from .exceptions import NoSuchOption +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import wrap_text +from .globals import get_current_context +from .parser import OptionParser +from .termui import clear +from .termui import confirm +from .termui import echo_via_pager +from .termui import edit +from .termui import get_terminal_size +from .termui import getchar +from .termui import launch +from .termui import pause +from .termui import progressbar +from .termui import prompt +from .termui import secho +from .termui import style +from .termui import unstyle +from .types import BOOL +from .types import Choice +from .types import DateTime +from .types import File +from .types import FLOAT +from .types import FloatRange +from .types import INT +from .types import IntRange +from .types import ParamType +from .types import Path +from .types import STRING +from .types import Tuple +from .types import UNPROCESSED +from .types import UUID +from .utils import echo +from .utils import format_filename +from .utils import get_app_dir +from .utils import get_binary_stream +from .utils import get_os_args +from .utils import get_text_stream +from .utils import open_file + +# Controls if click should emit the warning about the use of unicode +# literals. +disable_unicode_literals_warning = False + +__version__ = "7.1.2" diff --git a/openpype/vendor/python/python_2/click/_bashcomplete.py b/openpype/vendor/python/python_2/click/_bashcomplete.py new file mode 100644 index 0000000000..8bca24480f --- /dev/null +++ b/openpype/vendor/python/python_2/click/_bashcomplete.py @@ -0,0 +1,375 @@ +import copy +import os +import re + +from .core import Argument +from .core import MultiCommand +from .core import Option +from .parser import split_arg_string +from .types import Choice +from .utils import echo + +try: + from collections import abc +except ImportError: + import collections as abc + +WORDBREAK = "=" + +# Note, only BASH version 4.4 and later have the nosort option. +COMPLETION_SCRIPT_BASH = """ +%(complete_func)s() { + local IFS=$'\n' + COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ + COMP_CWORD=$COMP_CWORD \\ + %(autocomplete_var)s=complete $1 ) ) + return 0 +} + +%(complete_func)setup() { + local COMPLETION_OPTIONS="" + local BASH_VERSION_ARR=(${BASH_VERSION//./ }) + # Only BASH version 4.4 and later have the nosort option. + if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \ +&& [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then + COMPLETION_OPTIONS="-o nosort" + fi + + complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s +} + +%(complete_func)setup +""" + +COMPLETION_SCRIPT_ZSH = """ +#compdef %(script_names)s + +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + (( ! $+commands[%(script_names)s] )) && return 1 + + response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\ + COMP_CWORD=$((CURRENT-1)) \\ + %(autocomplete_var)s=\"complete_zsh\" \\ + %(script_names)s )}") + + for key descr in ${(kv)response}; do + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -a completions + fi + compstate[insert]="automenu" +} + +compdef %(complete_func)s %(script_names)s +""" + +COMPLETION_SCRIPT_FISH = ( + "complete --no-files --command %(script_names)s --arguments" + ' "(env %(autocomplete_var)s=complete_fish' + " COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)" + ' %(script_names)s)"' +) + +_completion_scripts = { + "bash": COMPLETION_SCRIPT_BASH, + "zsh": COMPLETION_SCRIPT_ZSH, + "fish": COMPLETION_SCRIPT_FISH, +} + +_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]") + + +def get_completion_script(prog_name, complete_var, shell): + cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_")) + script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH) + return ( + script + % { + "complete_func": "_{}_completion".format(cf_name), + "script_names": prog_name, + "autocomplete_var": complete_var, + } + ).strip() + ";" + + +def resolve_ctx(cli, prog_name, args): + """Parse into a hierarchy of contexts. Contexts are connected + through the parent variable. + + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :return: the final context/command parsed + """ + ctx = cli.make_context(prog_name, args, resilient_parsing=True) + args = ctx.protected_args + ctx.args + while args: + if isinstance(ctx.command, MultiCommand): + if not ctx.command.chain: + cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) + if cmd is None: + return ctx + ctx = cmd.make_context( + cmd_name, args, parent=ctx, resilient_parsing=True + ) + args = ctx.protected_args + ctx.args + else: + # Walk chained subcommand contexts saving the last one. + while args: + cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) + if cmd is None: + return ctx + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True, + ) + args = sub_ctx.args + ctx = sub_ctx + args = sub_ctx.protected_args + sub_ctx.args + else: + break + return ctx + + +def start_of_option(param_str): + """ + :param param_str: param_str to check + :return: whether or not this is the start of an option declaration + (i.e. starts "-" or "--") + """ + return param_str and param_str[:1] == "-" + + +def is_incomplete_option(all_args, cmd_param): + """ + :param all_args: the full original list of args supplied + :param cmd_param: the current command paramter + :return: whether or not the last option declaration (i.e. starts + "-" or "--") is incomplete and corresponds to this cmd_param. In + other words whether this cmd_param option can still accept + values + """ + if not isinstance(cmd_param, Option): + return False + if cmd_param.is_flag: + return False + last_option = None + for index, arg_str in enumerate( + reversed([arg for arg in all_args if arg != WORDBREAK]) + ): + if index + 1 > cmd_param.nargs: + break + if start_of_option(arg_str): + last_option = arg_str + + return True if last_option and last_option in cmd_param.opts else False + + +def is_incomplete_argument(current_params, cmd_param): + """ + :param current_params: the current params and values for this + argument as already entered + :param cmd_param: the current command parameter + :return: whether or not the last argument is incomplete and + corresponds to this cmd_param. In other words whether or not the + this cmd_param argument can still accept values + """ + if not isinstance(cmd_param, Argument): + return False + current_param_values = current_params[cmd_param.name] + if current_param_values is None: + return True + if cmd_param.nargs == -1: + return True + if ( + isinstance(current_param_values, abc.Iterable) + and cmd_param.nargs > 1 + and len(current_param_values) < cmd_param.nargs + ): + return True + return False + + +def get_user_autocompletions(ctx, args, incomplete, cmd_param): + """ + :param ctx: context associated with the parsed command + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :param cmd_param: command definition + :return: all the possible user-specified completions for the param + """ + results = [] + if isinstance(cmd_param.type, Choice): + # Choices don't support descriptions. + results = [ + (c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete) + ] + elif cmd_param.autocompletion is not None: + dynamic_completions = cmd_param.autocompletion( + ctx=ctx, args=args, incomplete=incomplete + ) + results = [ + c if isinstance(c, tuple) else (c, None) for c in dynamic_completions + ] + return results + + +def get_visible_commands_starting_with(ctx, starts_with): + """ + :param ctx: context associated with the parsed command + :starts_with: string that visible commands must start with. + :return: all visible (not hidden) commands that start with starts_with. + """ + for c in ctx.command.list_commands(ctx): + if c.startswith(starts_with): + command = ctx.command.get_command(ctx, c) + if not command.hidden: + yield command + + +def add_subcommand_completions(ctx, incomplete, completions_out): + # Add subcommand completions. + if isinstance(ctx.command, MultiCommand): + completions_out.extend( + [ + (c.name, c.get_short_help_str()) + for c in get_visible_commands_starting_with(ctx, incomplete) + ] + ) + + # Walk up the context list and add any other completion + # possibilities from chained commands + while ctx.parent is not None: + ctx = ctx.parent + if isinstance(ctx.command, MultiCommand) and ctx.command.chain: + remaining_commands = [ + c + for c in get_visible_commands_starting_with(ctx, incomplete) + if c.name not in ctx.protected_args + ] + completions_out.extend( + [(c.name, c.get_short_help_str()) for c in remaining_commands] + ) + + +def get_choices(cli, prog_name, args, incomplete): + """ + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :return: all the possible completions for the incomplete + """ + all_args = copy.deepcopy(args) + + ctx = resolve_ctx(cli, prog_name, args) + if ctx is None: + return [] + + has_double_dash = "--" in all_args + + # In newer versions of bash long opts with '='s are partitioned, but + # it's easier to parse without the '=' + if start_of_option(incomplete) and WORDBREAK in incomplete: + partition_incomplete = incomplete.partition(WORDBREAK) + all_args.append(partition_incomplete[0]) + incomplete = partition_incomplete[2] + elif incomplete == WORDBREAK: + incomplete = "" + + completions = [] + if not has_double_dash and start_of_option(incomplete): + # completions for partial options + for param in ctx.command.params: + if isinstance(param, Option) and not param.hidden: + param_opts = [ + param_opt + for param_opt in param.opts + param.secondary_opts + if param_opt not in all_args or param.multiple + ] + completions.extend( + [(o, param.help) for o in param_opts if o.startswith(incomplete)] + ) + return completions + # completion for option values from user supplied values + for param in ctx.command.params: + if is_incomplete_option(all_args, param): + return get_user_autocompletions(ctx, all_args, incomplete, param) + # completion for argument values from user supplied values + for param in ctx.command.params: + if is_incomplete_argument(ctx.params, param): + return get_user_autocompletions(ctx, all_args, incomplete, param) + + add_subcommand_completions(ctx, incomplete, completions) + # Sort before returning so that proper ordering can be enforced in custom types. + return sorted(completions) + + +def do_complete(cli, prog_name, include_descriptions): + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + for item in get_choices(cli, prog_name, args, incomplete): + echo(item[0]) + if include_descriptions: + # ZSH has trouble dealing with empty array parameters when + # returned from commands, use '_' to indicate no description + # is present. + echo(item[1] if item[1] else "_") + + return True + + +def do_complete_fish(cli, prog_name): + cwords = split_arg_string(os.environ["COMP_WORDS"]) + incomplete = os.environ["COMP_CWORD"] + args = cwords[1:] + + for item in get_choices(cli, prog_name, args, incomplete): + if item[1]: + echo("{arg}\t{desc}".format(arg=item[0], desc=item[1])) + else: + echo(item[0]) + + return True + + +def bashcomplete(cli, prog_name, complete_var, complete_instr): + if "_" in complete_instr: + command, shell = complete_instr.split("_", 1) + else: + command = complete_instr + shell = "bash" + + if command == "source": + echo(get_completion_script(prog_name, complete_var, shell)) + return True + elif command == "complete": + if shell == "fish": + return do_complete_fish(cli, prog_name) + elif shell in {"bash", "zsh"}: + return do_complete(cli, prog_name, shell == "zsh") + + return False diff --git a/openpype/vendor/python/python_2/click/_compat.py b/openpype/vendor/python/python_2/click/_compat.py new file mode 100644 index 0000000000..60cb115bc5 --- /dev/null +++ b/openpype/vendor/python/python_2/click/_compat.py @@ -0,0 +1,786 @@ +# flake8: noqa +import codecs +import io +import os +import re +import sys +from weakref import WeakKeyDictionary + +PY2 = sys.version_info[0] == 2 +CYGWIN = sys.platform.startswith("cygwin") +MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version) +# Determine local App Engine environment, per Google's own suggestion +APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.get( + "SERVER_SOFTWARE", "" +) +WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2 +DEFAULT_COLUMNS = 80 + + +_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") + + +def get_filesystem_encoding(): + return sys.getfilesystemencoding() or sys.getdefaultencoding() + + +def _make_text_stream( + stream, encoding, errors, force_readable=False, force_writable=False +): + if encoding is None: + encoding = get_best_encoding(stream) + if errors is None: + errors = "replace" + return _NonClosingTextIOWrapper( + stream, + encoding, + errors, + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def is_ascii_encoding(encoding): + """Checks if a given encoding is ascii.""" + try: + return codecs.lookup(encoding).name == "ascii" + except LookupError: + return False + + +def get_best_encoding(stream): + """Returns the default stream encoding if not found.""" + rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() + if is_ascii_encoding(rv): + return "utf-8" + return rv + + +class _NonClosingTextIOWrapper(io.TextIOWrapper): + def __init__( + self, + stream, + encoding, + errors, + force_readable=False, + force_writable=False, + **extra + ): + self._stream = stream = _FixupStream(stream, force_readable, force_writable) + io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra) + + # The io module is a place where the Python 3 text behavior + # was forced upon Python 2, so we need to unbreak + # it to look like Python 2. + if PY2: + + def write(self, x): + if isinstance(x, str) or is_bytes(x): + try: + self.flush() + except Exception: + pass + return self.buffer.write(str(x)) + return io.TextIOWrapper.write(self, x) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def __del__(self): + try: + self.detach() + except Exception: + pass + + def isatty(self): + # https://bitbucket.org/pypy/pypy/issue/1803 + return self._stream.isatty() + + +class _FixupStream(object): + """The new io interface needs more from streams than streams + traditionally implement. As such, this fix-up code is necessary in + some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). + """ + + def __init__(self, stream, force_readable=False, force_writable=False): + self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable + + def __getattr__(self, name): + return getattr(self._stream, name) + + def read1(self, size): + f = getattr(self._stream, "read1", None) + if f is not None: + return f(size) + # We only dispatch to readline instead of read in Python 2 as we + # do not want cause problems with the different implementation + # of line buffering. + if PY2: + return self._stream.readline(size) + return self._stream.read(size) + + def readable(self): + if self._force_readable: + return True + x = getattr(self._stream, "readable", None) + if x is not None: + return x() + try: + self._stream.read(0) + except Exception: + return False + return True + + def writable(self): + if self._force_writable: + return True + x = getattr(self._stream, "writable", None) + if x is not None: + return x() + try: + self._stream.write("") + except Exception: + try: + self._stream.write(b"") + except Exception: + return False + return True + + def seekable(self): + x = getattr(self._stream, "seekable", None) + if x is not None: + return x() + try: + self._stream.seek(self._stream.tell()) + except Exception: + return False + return True + + +if PY2: + text_type = unicode + raw_input = raw_input + string_types = (str, unicode) + int_types = (int, long) + iteritems = lambda x: x.iteritems() + range_type = xrange + + def is_bytes(x): + return isinstance(x, (buffer, bytearray)) + + _identifier_re = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$") + + # For Windows, we need to force stdout/stdin/stderr to binary if it's + # fetched for that. This obviously is not the most correct way to do + # it as it changes global state. Unfortunately, there does not seem to + # be a clear better way to do it as just reopening the file in binary + # mode does not change anything. + # + # An option would be to do what Python 3 does and to open the file as + # binary only, patch it back to the system, and then use a wrapper + # stream that converts newlines. It's not quite clear what's the + # correct option here. + # + # This code also lives in _winconsole for the fallback to the console + # emulation stream. + # + # There are also Windows environments where the `msvcrt` module is not + # available (which is why we use try-catch instead of the WIN variable + # here), such as the Google App Engine development server on Windows. In + # those cases there is just nothing we can do. + def set_binary_mode(f): + return f + + try: + import msvcrt + except ImportError: + pass + else: + + def set_binary_mode(f): + try: + fileno = f.fileno() + except Exception: + pass + else: + msvcrt.setmode(fileno, os.O_BINARY) + return f + + try: + import fcntl + except ImportError: + pass + else: + + def set_binary_mode(f): + try: + fileno = f.fileno() + except Exception: + pass + else: + flags = fcntl.fcntl(fileno, fcntl.F_GETFL) + fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + return f + + def isidentifier(x): + return _identifier_re.search(x) is not None + + def get_binary_stdin(): + return set_binary_mode(sys.stdin) + + def get_binary_stdout(): + _wrap_std_stream("stdout") + return set_binary_mode(sys.stdout) + + def get_binary_stderr(): + _wrap_std_stream("stderr") + return set_binary_mode(sys.stderr) + + def get_text_stdin(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _make_text_stream(sys.stdin, encoding, errors, force_readable=True) + + def get_text_stdout(encoding=None, errors=None): + _wrap_std_stream("stdout") + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _make_text_stream(sys.stdout, encoding, errors, force_writable=True) + + def get_text_stderr(encoding=None, errors=None): + _wrap_std_stream("stderr") + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _make_text_stream(sys.stderr, encoding, errors, force_writable=True) + + def filename_to_ui(value): + if isinstance(value, bytes): + value = value.decode(get_filesystem_encoding(), "replace") + return value + + +else: + import io + + text_type = str + raw_input = input + string_types = (str,) + int_types = (int,) + range_type = range + isidentifier = lambda x: x.isidentifier() + iteritems = lambda x: iter(x.items()) + + def is_bytes(x): + return isinstance(x, (bytes, memoryview, bytearray)) + + def _is_binary_reader(stream, default=False): + try: + return isinstance(stream.read(0), bytes) + except Exception: + return default + # This happens in some cases where the stream was already + # closed. In this case, we assume the default. + + def _is_binary_writer(stream, default=False): + try: + stream.write(b"") + except Exception: + try: + stream.write("") + return False + except Exception: + pass + return default + return True + + def _find_binary_reader(stream): + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_reader(stream, False): + return stream + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_reader(buf, True): + return buf + + def _find_binary_writer(stream): + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detatching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_writer(stream, False): + return stream + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_writer(buf, True): + return buf + + def _stream_is_misconfigured(stream): + """A stream is misconfigured if its encoding is ASCII.""" + # If the stream does not have an encoding set, we assume it's set + # to ASCII. This appears to happen in certain unittest + # environments. It's not quite clear what the correct behavior is + # but this at least will force Click to recover somehow. + return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") + + def _is_compat_stream_attr(stream, attr, value): + """A stream attribute is compatible if it is equal to the + desired value or the desired value is unset and the attribute + has a value. + """ + stream_value = getattr(stream, attr, None) + return stream_value == value or (value is None and stream_value is not None) + + def _is_compatible_text_stream(stream, encoding, errors): + """Check if a stream's encoding and errors attributes are + compatible with the desired values. + """ + return _is_compat_stream_attr( + stream, "encoding", encoding + ) and _is_compat_stream_attr(stream, "errors", errors) + + def _force_correct_text_stream( + text_stream, + encoding, + errors, + is_binary, + find_binary, + force_readable=False, + force_writable=False, + ): + if is_binary(text_stream, False): + binary_reader = text_stream + else: + # If the stream looks compatible, and won't default to a + # misconfigured ascii encoding, return it as-is. + if _is_compatible_text_stream(text_stream, encoding, errors) and not ( + encoding is None and _stream_is_misconfigured(text_stream) + ): + return text_stream + + # Otherwise, get the underlying binary reader. + binary_reader = find_binary(text_stream) + + # If that's not possible, silently use the original reader + # and get mojibake instead of exceptions. + if binary_reader is None: + return text_stream + + # Default errors to replace instead of strict in order to get + # something that works. + if errors is None: + errors = "replace" + + # Wrap the binary stream in a text stream with the correct + # encoding parameters. + return _make_text_stream( + binary_reader, + encoding, + errors, + force_readable=force_readable, + force_writable=force_writable, + ) + + def _force_correct_text_reader(text_reader, encoding, errors, force_readable=False): + return _force_correct_text_stream( + text_reader, + encoding, + errors, + _is_binary_reader, + _find_binary_reader, + force_readable=force_readable, + ) + + def _force_correct_text_writer(text_writer, encoding, errors, force_writable=False): + return _force_correct_text_stream( + text_writer, + encoding, + errors, + _is_binary_writer, + _find_binary_writer, + force_writable=force_writable, + ) + + def get_binary_stdin(): + reader = _find_binary_reader(sys.stdin) + if reader is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdin.") + return reader + + def get_binary_stdout(): + writer = _find_binary_writer(sys.stdout) + if writer is None: + raise RuntimeError( + "Was not able to determine binary stream for sys.stdout." + ) + return writer + + def get_binary_stderr(): + writer = _find_binary_writer(sys.stderr) + if writer is None: + raise RuntimeError( + "Was not able to determine binary stream for sys.stderr." + ) + return writer + + def get_text_stdin(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_reader( + sys.stdin, encoding, errors, force_readable=True + ) + + def get_text_stdout(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer( + sys.stdout, encoding, errors, force_writable=True + ) + + def get_text_stderr(encoding=None, errors=None): + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer( + sys.stderr, encoding, errors, force_writable=True + ) + + def filename_to_ui(value): + if isinstance(value, bytes): + value = value.decode(get_filesystem_encoding(), "replace") + else: + value = value.encode("utf-8", "surrogateescape").decode("utf-8", "replace") + return value + + +def get_streerror(e, default=None): + if hasattr(e, "strerror"): + msg = e.strerror + else: + if default is not None: + msg = default + else: + msg = str(e) + if isinstance(msg, bytes): + msg = msg.decode("utf-8", "replace") + return msg + + +def _wrap_io_open(file, mode, encoding, errors): + """On Python 2, :func:`io.open` returns a text file wrapper that + requires passing ``unicode`` to ``write``. Need to open the file in + binary mode then wrap it in a subclass that can write ``str`` and + ``unicode``. + + Also handles not passing ``encoding`` and ``errors`` in binary mode. + """ + binary = "b" in mode + + if binary: + kwargs = {} + else: + kwargs = {"encoding": encoding, "errors": errors} + + if not PY2 or binary: + return io.open(file, mode, **kwargs) + + f = io.open(file, "{}b".format(mode.replace("t", ""))) + return _make_text_stream(f, **kwargs) + + +def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False): + binary = "b" in mode + + # Standard streams first. These are simple because they don't need + # special handling for the atomic flag. It's entirely ignored. + if filename == "-": + if any(m in mode for m in ["w", "a", "x"]): + if binary: + return get_binary_stdout(), False + return get_text_stdout(encoding=encoding, errors=errors), False + if binary: + return get_binary_stdin(), False + return get_text_stdin(encoding=encoding, errors=errors), False + + # Non-atomic writes directly go out through the regular open functions. + if not atomic: + return _wrap_io_open(filename, mode, encoding, errors), True + + # Some usability stuff for atomic writes + if "a" in mode: + raise ValueError( + "Appending to an existing file is not supported, because that" + " would involve an expensive `copy`-operation to a temporary" + " file. Open the file in normal `w`-mode and copy explicitly" + " if that's what you're after." + ) + if "x" in mode: + raise ValueError("Use the `overwrite`-parameter instead.") + if "w" not in mode: + raise ValueError("Atomic writes only make sense with `w`-mode.") + + # Atomic writes are more complicated. They work by opening a file + # as a proxy in the same folder and then using the fdopen + # functionality to wrap it in a Python file. Then we wrap it in an + # atomic file that moves the file over on close. + import errno + import random + + try: + perm = os.stat(filename).st_mode + except OSError: + perm = None + + flags = os.O_RDWR | os.O_CREAT | os.O_EXCL + + if binary: + flags |= getattr(os, "O_BINARY", 0) + + while True: + tmp_filename = os.path.join( + os.path.dirname(filename), + ".__atomic-write{:08x}".format(random.randrange(1 << 32)), + ) + try: + fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) + break + except OSError as e: + if e.errno == errno.EEXIST or ( + os.name == "nt" + and e.errno == errno.EACCES + and os.path.isdir(e.filename) + and os.access(e.filename, os.W_OK) + ): + continue + raise + + if perm is not None: + os.chmod(tmp_filename, perm) # in case perm includes bits in umask + + f = _wrap_io_open(fd, mode, encoding, errors) + return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True + + +# Used in a destructor call, needs extra protection from interpreter cleanup. +if hasattr(os, "replace"): + _replace = os.replace + _can_replace = True +else: + _replace = os.rename + _can_replace = not WIN + + +class _AtomicFile(object): + def __init__(self, f, tmp_filename, real_filename): + self._f = f + self._tmp_filename = tmp_filename + self._real_filename = real_filename + self.closed = False + + @property + def name(self): + return self._real_filename + + def close(self, delete=False): + if self.closed: + return + self._f.close() + if not _can_replace: + try: + os.remove(self._real_filename) + except OSError: + pass + _replace(self._tmp_filename, self._real_filename) + self.closed = True + + def __getattr__(self, name): + return getattr(self._f, name) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + self.close(delete=exc_type is not None) + + def __repr__(self): + return repr(self._f) + + +auto_wrap_for_ansi = None +colorama = None +get_winterm_size = None + + +def strip_ansi(value): + return _ansi_re.sub("", value) + + +def _is_jupyter_kernel_output(stream): + if WIN: + # TODO: Couldn't test on Windows, should't try to support until + # someone tests the details wrt colorama. + return + + while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): + stream = stream._stream + + return stream.__class__.__module__.startswith("ipykernel.") + + +def should_strip_ansi(stream=None, color=None): + if color is None: + if stream is None: + stream = sys.stdin + return not isatty(stream) and not _is_jupyter_kernel_output(stream) + return not color + + +# If we're on Windows, we provide transparent integration through +# colorama. This will make ANSI colors through the echo function +# work automatically. +if WIN: + # Windows has a smaller terminal + DEFAULT_COLUMNS = 79 + + from ._winconsole import _get_windows_console_stream, _wrap_std_stream + + def _get_argv_encoding(): + import locale + + return locale.getpreferredencoding() + + if PY2: + + def raw_input(prompt=""): + sys.stderr.flush() + if prompt: + stdout = _default_text_stdout() + stdout.write(prompt) + stdin = _default_text_stdin() + return stdin.readline().rstrip("\r\n") + + try: + import colorama + except ImportError: + pass + else: + _ansi_stream_wrappers = WeakKeyDictionary() + + def auto_wrap_for_ansi(stream, color=None): + """This function wraps a stream so that calls through colorama + are issued to the win32 console API to recolor on demand. It + also ensures to reset the colors if a write call is interrupted + to not destroy the console afterwards. + """ + try: + cached = _ansi_stream_wrappers.get(stream) + except Exception: + cached = None + if cached is not None: + return cached + strip = should_strip_ansi(stream, color) + ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) + rv = ansi_wrapper.stream + _write = rv.write + + def _safe_write(s): + try: + return _write(s) + except: + ansi_wrapper.reset_all() + raise + + rv.write = _safe_write + try: + _ansi_stream_wrappers[stream] = rv + except Exception: + pass + return rv + + def get_winterm_size(): + win = colorama.win32.GetConsoleScreenBufferInfo( + colorama.win32.STDOUT + ).srWindow + return win.Right - win.Left, win.Bottom - win.Top + + +else: + + def _get_argv_encoding(): + return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding() + + _get_windows_console_stream = lambda *x: None + _wrap_std_stream = lambda *x: None + + +def term_len(x): + return len(strip_ansi(x)) + + +def isatty(stream): + try: + return stream.isatty() + except Exception: + return False + + +def _make_cached_stream_func(src_func, wrapper_func): + cache = WeakKeyDictionary() + + def func(): + stream = src_func() + try: + rv = cache.get(stream) + except Exception: + rv = None + if rv is not None: + return rv + rv = wrapper_func() + try: + stream = src_func() # In case wrapper_func() modified the stream + cache[stream] = rv + except Exception: + pass + return rv + + return func + + +_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin) +_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout) +_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) + + +binary_streams = { + "stdin": get_binary_stdin, + "stdout": get_binary_stdout, + "stderr": get_binary_stderr, +} + +text_streams = { + "stdin": get_text_stdin, + "stdout": get_text_stdout, + "stderr": get_text_stderr, +} diff --git a/openpype/vendor/python/python_2/click/_termui_impl.py b/openpype/vendor/python/python_2/click/_termui_impl.py new file mode 100644 index 0000000000..88bec37701 --- /dev/null +++ b/openpype/vendor/python/python_2/click/_termui_impl.py @@ -0,0 +1,657 @@ +# -*- coding: utf-8 -*- +""" +This module contains implementations for the termui module. To keep the +import time of Click down, some infrequently used functionality is +placed in this module and only imported as needed. +""" +import contextlib +import math +import os +import sys +import time + +from ._compat import _default_text_stdout +from ._compat import CYGWIN +from ._compat import get_best_encoding +from ._compat import int_types +from ._compat import isatty +from ._compat import open_stream +from ._compat import range_type +from ._compat import strip_ansi +from ._compat import term_len +from ._compat import WIN +from .exceptions import ClickException +from .utils import echo + +if os.name == "nt": + BEFORE_BAR = "\r" + AFTER_BAR = "\n" +else: + BEFORE_BAR = "\r\033[?25l" + AFTER_BAR = "\033[?25h\n" + + +def _length_hint(obj): + """Returns the length hint of an object.""" + try: + return len(obj) + except (AttributeError, TypeError): + try: + get_hint = type(obj).__length_hint__ + except AttributeError: + return None + try: + hint = get_hint(obj) + except TypeError: + return None + if hint is NotImplemented or not isinstance(hint, int_types) or hint < 0: + return None + return hint + + +class ProgressBar(object): + def __init__( + self, + iterable, + length=None, + fill_char="#", + empty_char=" ", + bar_template="%(bar)s", + info_sep=" ", + show_eta=True, + show_percent=None, + show_pos=False, + item_show_func=None, + label=None, + file=None, + color=None, + width=30, + ): + self.fill_char = fill_char + self.empty_char = empty_char + self.bar_template = bar_template + self.info_sep = info_sep + self.show_eta = show_eta + self.show_percent = show_percent + self.show_pos = show_pos + self.item_show_func = item_show_func + self.label = label or "" + if file is None: + file = _default_text_stdout() + self.file = file + self.color = color + self.width = width + self.autowidth = width == 0 + + if length is None: + length = _length_hint(iterable) + if iterable is None: + if length is None: + raise TypeError("iterable or length is required") + iterable = range_type(length) + self.iter = iter(iterable) + self.length = length + self.length_known = length is not None + self.pos = 0 + self.avg = [] + self.start = self.last_eta = time.time() + self.eta_known = False + self.finished = False + self.max_width = None + self.entered = False + self.current_item = None + self.is_hidden = not isatty(self.file) + self._last_line = None + self.short_limit = 0.5 + + def __enter__(self): + self.entered = True + self.render_progress() + return self + + def __exit__(self, exc_type, exc_value, tb): + self.render_finish() + + def __iter__(self): + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + self.render_progress() + return self.generator() + + def __next__(self): + # Iteration is defined in terms of a generator function, + # returned by iter(self); use that to define next(). This works + # because `self.iter` is an iterable consumed by that generator, + # so it is re-entry safe. Calling `next(self.generator())` + # twice works and does "what you want". + return next(iter(self)) + + # Python 2 compat + next = __next__ + + def is_fast(self): + return time.time() - self.start <= self.short_limit + + def render_finish(self): + if self.is_hidden or self.is_fast(): + return + self.file.write(AFTER_BAR) + self.file.flush() + + @property + def pct(self): + if self.finished: + return 1.0 + return min(self.pos / (float(self.length) or 1), 1.0) + + @property + def time_per_iteration(self): + if not self.avg: + return 0.0 + return sum(self.avg) / float(len(self.avg)) + + @property + def eta(self): + if self.length_known and not self.finished: + return self.time_per_iteration * (self.length - self.pos) + return 0.0 + + def format_eta(self): + if self.eta_known: + t = int(self.eta) + seconds = t % 60 + t //= 60 + minutes = t % 60 + t //= 60 + hours = t % 24 + t //= 24 + if t > 0: + return "{}d {:02}:{:02}:{:02}".format(t, hours, minutes, seconds) + else: + return "{:02}:{:02}:{:02}".format(hours, minutes, seconds) + return "" + + def format_pos(self): + pos = str(self.pos) + if self.length_known: + pos += "/{}".format(self.length) + return pos + + def format_pct(self): + return "{: 4}%".format(int(self.pct * 100))[1:] + + def format_bar(self): + if self.length_known: + bar_length = int(self.pct * self.width) + bar = self.fill_char * bar_length + bar += self.empty_char * (self.width - bar_length) + elif self.finished: + bar = self.fill_char * self.width + else: + bar = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + bar[ + int( + (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5) + * self.width + ) + ] = self.fill_char + bar = "".join(bar) + return bar + + def format_progress_line(self): + show_percent = self.show_percent + + info_bits = [] + if self.length_known and show_percent is None: + show_percent = not self.show_pos + + if self.show_pos: + info_bits.append(self.format_pos()) + if show_percent: + info_bits.append(self.format_pct()) + if self.show_eta and self.eta_known and not self.finished: + info_bits.append(self.format_eta()) + if self.item_show_func is not None: + item_info = self.item_show_func(self.current_item) + if item_info is not None: + info_bits.append(item_info) + + return ( + self.bar_template + % { + "label": self.label, + "bar": self.format_bar(), + "info": self.info_sep.join(info_bits), + } + ).rstrip() + + def render_progress(self): + from .termui import get_terminal_size + + if self.is_hidden: + return + + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, get_terminal_size()[0] - clutter_length) + if new_width < old_width: + buf.append(BEFORE_BAR) + buf.append(" " * self.max_width) + self.max_width = new_width + self.width = new_width + + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width + + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(" " * (clear_width - line_len)) + line = "".join(buf) + # Render the line only if it changed. + + if line != self._last_line and not self.is_fast(): + self._last_line = line + echo(line, file=self.file, color=self.color, nl=False) + self.file.flush() + + def make_step(self, n_steps): + self.pos += n_steps + if self.length_known and self.pos >= self.length: + self.finished = True + + if (time.time() - self.last_eta) < 1.0: + return + + self.last_eta = time.time() + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] + + self.eta_known = self.length_known + + def update(self, n_steps): + self.make_step(n_steps) + self.render_progress() + + def finish(self): + self.eta_known = 0 + self.current_item = None + self.finished = True + + def generator(self): + """Return a generator which yields the items added to the bar + during construction, and updates the progress bar *after* the + yielded block returns. + """ + # WARNING: the iterator interface for `ProgressBar` relies on + # this and only works because this is a simple generator which + # doesn't create or manage additional state. If this function + # changes, the impact should be evaluated both against + # `iter(bar)` and `next(bar)`. `next()` in particular may call + # `self.generator()` repeatedly, and this must remain safe in + # order for that interface to work. + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + + if self.is_hidden: + for rv in self.iter: + yield rv + else: + for rv in self.iter: + self.current_item = rv + yield rv + self.update(1) + self.finish() + self.render_progress() + + +def pager(generator, color=None): + """Decide what method to use for paging through text.""" + stdout = _default_text_stdout() + if not isatty(sys.stdin) or not isatty(stdout): + return _nullpager(stdout, generator, color) + pager_cmd = (os.environ.get("PAGER", None) or "").strip() + if pager_cmd: + if WIN: + return _tempfilepager(generator, pager_cmd, color) + return _pipepager(generator, pager_cmd, color) + if os.environ.get("TERM") in ("dumb", "emacs"): + return _nullpager(stdout, generator, color) + if WIN or sys.platform.startswith("os2"): + return _tempfilepager(generator, "more <", color) + if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0: + return _pipepager(generator, "less", color) + + import tempfile + + fd, filename = tempfile.mkstemp() + os.close(fd) + try: + if hasattr(os, "system") and os.system('more "{}"'.format(filename)) == 0: + return _pipepager(generator, "more", color) + return _nullpager(stdout, generator, color) + finally: + os.unlink(filename) + + +def _pipepager(generator, cmd, color): + """Page through text by feeding it to another program. Invoking a + pager through this might support colors. + """ + import subprocess + + env = dict(os.environ) + + # If we're piping to less we might support colors under the + # condition that + cmd_detail = cmd.rsplit("/", 1)[-1].split() + if color is None and cmd_detail[0] == "less": + less_flags = "{}{}".format(os.environ.get("LESS", ""), " ".join(cmd_detail[1:])) + if not less_flags: + env["LESS"] = "-R" + color = True + elif "r" in less_flags or "R" in less_flags: + color = True + + c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env) + encoding = get_best_encoding(c.stdin) + try: + for text in generator: + if not color: + text = strip_ansi(text) + + c.stdin.write(text.encode(encoding, "replace")) + except (IOError, KeyboardInterrupt): + pass + else: + c.stdin.close() + + # Less doesn't respect ^C, but catches it for its own UI purposes (aborting + # search or other commands inside less). + # + # That means when the user hits ^C, the parent process (click) terminates, + # but less is still alive, paging the output and messing up the terminal. + # + # If the user wants to make the pager exit on ^C, they should set + # `LESS='-K'`. It's not our decision to make. + while True: + try: + c.wait() + except KeyboardInterrupt: + pass + else: + break + + +def _tempfilepager(generator, cmd, color): + """Page through text by invoking a program on a temporary file.""" + import tempfile + + filename = tempfile.mktemp() + # TODO: This never terminates if the passed generator never terminates. + text = "".join(generator) + if not color: + text = strip_ansi(text) + encoding = get_best_encoding(sys.stdout) + with open_stream(filename, "wb")[0] as f: + f.write(text.encode(encoding)) + try: + os.system('{} "{}"'.format(cmd, filename)) + finally: + os.unlink(filename) + + +def _nullpager(stream, generator, color): + """Simply print unformatted text. This is the ultimate fallback.""" + for text in generator: + if not color: + text = strip_ansi(text) + stream.write(text) + + +class Editor(object): + def __init__(self, editor=None, env=None, require_save=True, extension=".txt"): + self.editor = editor + self.env = env + self.require_save = require_save + self.extension = extension + + def get_editor(self): + if self.editor is not None: + return self.editor + for key in "VISUAL", "EDITOR": + rv = os.environ.get(key) + if rv: + return rv + if WIN: + return "notepad" + for editor in "sensible-editor", "vim", "nano": + if os.system("which {} >/dev/null 2>&1".format(editor)) == 0: + return editor + return "vi" + + def edit_file(self, filename): + import subprocess + + editor = self.get_editor() + if self.env: + environ = os.environ.copy() + environ.update(self.env) + else: + environ = None + try: + c = subprocess.Popen( + '{} "{}"'.format(editor, filename), env=environ, shell=True, + ) + exit_code = c.wait() + if exit_code != 0: + raise ClickException("{}: Editing failed!".format(editor)) + except OSError as e: + raise ClickException("{}: Editing failed: {}".format(editor, e)) + + def edit(self, text): + import tempfile + + text = text or "" + if text and not text.endswith("\n"): + text += "\n" + + fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension) + try: + if WIN: + encoding = "utf-8-sig" + text = text.replace("\n", "\r\n") + else: + encoding = "utf-8" + text = text.encode(encoding) + + f = os.fdopen(fd, "wb") + f.write(text) + f.close() + timestamp = os.path.getmtime(name) + + self.edit_file(name) + + if self.require_save and os.path.getmtime(name) == timestamp: + return None + + f = open(name, "rb") + try: + rv = f.read() + finally: + f.close() + return rv.decode("utf-8-sig").replace("\r\n", "\n") + finally: + os.unlink(name) + + +def open_url(url, wait=False, locate=False): + import subprocess + + def _unquote_file(url): + try: + import urllib + except ImportError: + import urllib + if url.startswith("file://"): + url = urllib.unquote(url[7:]) + return url + + if sys.platform == "darwin": + args = ["open"] + if wait: + args.append("-W") + if locate: + args.append("-R") + args.append(_unquote_file(url)) + null = open("/dev/null", "w") + try: + return subprocess.Popen(args, stderr=null).wait() + finally: + null.close() + elif WIN: + if locate: + url = _unquote_file(url) + args = 'explorer /select,"{}"'.format(_unquote_file(url.replace('"', ""))) + else: + args = 'start {} "" "{}"'.format( + "/WAIT" if wait else "", url.replace('"', "") + ) + return os.system(args) + elif CYGWIN: + if locate: + url = _unquote_file(url) + args = 'cygstart "{}"'.format(os.path.dirname(url).replace('"', "")) + else: + args = 'cygstart {} "{}"'.format("-w" if wait else "", url.replace('"', "")) + return os.system(args) + + try: + if locate: + url = os.path.dirname(_unquote_file(url)) or "." + else: + url = _unquote_file(url) + c = subprocess.Popen(["xdg-open", url]) + if wait: + return c.wait() + return 0 + except OSError: + if url.startswith(("http://", "https://")) and not locate and not wait: + import webbrowser + + webbrowser.open(url) + return 0 + return 1 + + +def _translate_ch_to_exc(ch): + if ch == u"\x03": + raise KeyboardInterrupt() + if ch == u"\x04" and not WIN: # Unix-like, Ctrl+D + raise EOFError() + if ch == u"\x1a" and WIN: # Windows, Ctrl+Z + raise EOFError() + + +if WIN: + import msvcrt + + @contextlib.contextmanager + def raw_terminal(): + yield + + def getchar(echo): + # The function `getch` will return a bytes object corresponding to + # the pressed character. Since Windows 10 build 1803, it will also + # return \x00 when called a second time after pressing a regular key. + # + # `getwch` does not share this probably-bugged behavior. Moreover, it + # returns a Unicode object by default, which is what we want. + # + # Either of these functions will return \x00 or \xe0 to indicate + # a special key, and you need to call the same function again to get + # the "rest" of the code. The fun part is that \u00e0 is + # "latin small letter a with grave", so if you type that on a French + # keyboard, you _also_ get a \xe0. + # E.g., consider the Up arrow. This returns \xe0 and then \x48. The + # resulting Unicode string reads as "a with grave" + "capital H". + # This is indistinguishable from when the user actually types + # "a with grave" and then "capital H". + # + # When \xe0 is returned, we assume it's part of a special-key sequence + # and call `getwch` again, but that means that when the user types + # the \u00e0 character, `getchar` doesn't return until a second + # character is typed. + # The alternative is returning immediately, but that would mess up + # cross-platform handling of arrow keys and others that start with + # \xe0. Another option is using `getch`, but then we can't reliably + # read non-ASCII characters, because return values of `getch` are + # limited to the current 8-bit codepage. + # + # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` + # is doing the right thing in more situations than with `getch`. + if echo: + func = msvcrt.getwche + else: + func = msvcrt.getwch + + rv = func() + if rv in (u"\x00", u"\xe0"): + # \x00 and \xe0 are control characters that indicate special key, + # see above. + rv += func() + _translate_ch_to_exc(rv) + return rv + + +else: + import tty + import termios + + @contextlib.contextmanager + def raw_terminal(): + if not isatty(sys.stdin): + f = open("/dev/tty") + fd = f.fileno() + else: + fd = sys.stdin.fileno() + f = None + try: + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + yield fd + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + sys.stdout.flush() + if f is not None: + f.close() + except termios.error: + pass + + def getchar(echo): + with raw_terminal() as fd: + ch = os.read(fd, 32) + ch = ch.decode(get_best_encoding(sys.stdin), "replace") + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + _translate_ch_to_exc(ch) + return ch diff --git a/openpype/vendor/python/python_2/click/_textwrap.py b/openpype/vendor/python/python_2/click/_textwrap.py new file mode 100644 index 0000000000..6959087b7f --- /dev/null +++ b/openpype/vendor/python/python_2/click/_textwrap.py @@ -0,0 +1,37 @@ +import textwrap +from contextlib import contextmanager + + +class TextWrapper(textwrap.TextWrapper): + def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): + space_left = max(width - cur_len, 1) + + if self.break_long_words: + last = reversed_chunks[-1] + cut = last[:space_left] + res = last[space_left:] + cur_line.append(cut) + reversed_chunks[-1] = res + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + @contextmanager + def extra_indent(self, indent): + old_initial_indent = self.initial_indent + old_subsequent_indent = self.subsequent_indent + self.initial_indent += indent + self.subsequent_indent += indent + try: + yield + finally: + self.initial_indent = old_initial_indent + self.subsequent_indent = old_subsequent_indent + + def indent_only(self, text): + rv = [] + for idx, line in enumerate(text.splitlines()): + indent = self.initial_indent + if idx > 0: + indent = self.subsequent_indent + rv.append(indent + line) + return "\n".join(rv) diff --git a/openpype/vendor/python/python_2/click/_unicodefun.py b/openpype/vendor/python/python_2/click/_unicodefun.py new file mode 100644 index 0000000000..781c365227 --- /dev/null +++ b/openpype/vendor/python/python_2/click/_unicodefun.py @@ -0,0 +1,131 @@ +import codecs +import os +import sys + +from ._compat import PY2 + + +def _find_unicode_literals_frame(): + import __future__ + + if not hasattr(sys, "_getframe"): # not all Python implementations have it + return 0 + frm = sys._getframe(1) + idx = 1 + while frm is not None: + if frm.f_globals.get("__name__", "").startswith("click."): + frm = frm.f_back + idx += 1 + elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag: + return idx + else: + break + return 0 + + +def _check_for_unicode_literals(): + if not __debug__: + return + + from . import disable_unicode_literals_warning + + if not PY2 or disable_unicode_literals_warning: + return + bad_frame = _find_unicode_literals_frame() + if bad_frame <= 0: + return + from warnings import warn + + warn( + Warning( + "Click detected the use of the unicode_literals __future__" + " import. This is heavily discouraged because it can" + " introduce subtle bugs in your code. You should instead" + ' use explicit u"" literals for your unicode strings. For' + " more information see" + " https://click.palletsprojects.com/python3/" + ), + stacklevel=bad_frame, + ) + + +def _verify_python3_env(): + """Ensures that the environment is good for unicode on Python 3.""" + if PY2: + return + try: + import locale + + fs_enc = codecs.lookup(locale.getpreferredencoding()).name + except Exception: + fs_enc = "ascii" + if fs_enc != "ascii": + return + + extra = "" + if os.name == "posix": + import subprocess + + try: + rv = subprocess.Popen( + ["locale", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ).communicate()[0] + except OSError: + rv = b"" + good_locales = set() + has_c_utf8 = False + + # Make sure we're operating on text here. + if isinstance(rv, bytes): + rv = rv.decode("ascii", "replace") + + for line in rv.splitlines(): + locale = line.strip() + if locale.lower().endswith((".utf-8", ".utf8")): + good_locales.add(locale) + if locale.lower() in ("c.utf8", "c.utf-8"): + has_c_utf8 = True + + extra += "\n\n" + if not good_locales: + extra += ( + "Additional information: on this system no suitable" + " UTF-8 locales were discovered. This most likely" + " requires resolving by reconfiguring the locale" + " system." + ) + elif has_c_utf8: + extra += ( + "This system supports the C.UTF-8 locale which is" + " recommended. You might be able to resolve your issue" + " by exporting the following environment variables:\n\n" + " export LC_ALL=C.UTF-8\n" + " export LANG=C.UTF-8" + ) + else: + extra += ( + "This system lists a couple of UTF-8 supporting locales" + " that you can pick from. The following suitable" + " locales were discovered: {}".format(", ".join(sorted(good_locales))) + ) + + bad_locale = None + for locale in os.environ.get("LC_ALL"), os.environ.get("LANG"): + if locale and locale.lower().endswith((".utf-8", ".utf8")): + bad_locale = locale + if locale is not None: + break + if bad_locale is not None: + extra += ( + "\n\nClick discovered that you exported a UTF-8 locale" + " but the locale system could not pick up from it" + " because it does not exist. The exported locale is" + " '{}' but it is not supported".format(bad_locale) + ) + + raise RuntimeError( + "Click will abort further execution because Python 3 was" + " configured to use ASCII as encoding for the environment." + " Consult https://click.palletsprojects.com/python3/ for" + " mitigation steps.{}".format(extra) + ) diff --git a/openpype/vendor/python/python_2/click/_winconsole.py b/openpype/vendor/python/python_2/click/_winconsole.py new file mode 100644 index 0000000000..b6c4274af0 --- /dev/null +++ b/openpype/vendor/python/python_2/click/_winconsole.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +# This module is based on the excellent work by Adam Bartoš who +# provided a lot of what went into the implementation here in +# the discussion to issue1602 in the Python bug tracker. +# +# There are some general differences in regards to how this works +# compared to the original patches as we do not need to patch +# the entire interpreter but just work in our little world of +# echo and prmopt. +import ctypes +import io +import os +import sys +import time +import zlib +from ctypes import byref +from ctypes import c_char +from ctypes import c_char_p +from ctypes import c_int +from ctypes import c_ssize_t +from ctypes import c_ulong +from ctypes import c_void_p +from ctypes import POINTER +from ctypes import py_object +from ctypes import windll +from ctypes import WinError +from ctypes import WINFUNCTYPE +from ctypes.wintypes import DWORD +from ctypes.wintypes import HANDLE +from ctypes.wintypes import LPCWSTR +from ctypes.wintypes import LPWSTR + +import msvcrt + +from ._compat import _NonClosingTextIOWrapper +from ._compat import PY2 +from ._compat import text_type + +try: + from ctypes import pythonapi + + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release +except ImportError: + pythonapi = None + + +c_ssize_p = POINTER(c_ssize_t) + +kernel32 = windll.kernel32 +GetStdHandle = kernel32.GetStdHandle +ReadConsoleW = kernel32.ReadConsoleW +WriteConsoleW = kernel32.WriteConsoleW +GetConsoleMode = kernel32.GetConsoleMode +GetLastError = kernel32.GetLastError +GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) +CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( + ("CommandLineToArgvW", windll.shell32) +) +LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)( + ("LocalFree", windll.kernel32) +) + + +STDIN_HANDLE = GetStdHandle(-10) +STDOUT_HANDLE = GetStdHandle(-11) +STDERR_HANDLE = GetStdHandle(-12) + + +PyBUF_SIMPLE = 0 +PyBUF_WRITABLE = 1 + +ERROR_SUCCESS = 0 +ERROR_NOT_ENOUGH_MEMORY = 8 +ERROR_OPERATION_ABORTED = 995 + +STDIN_FILENO = 0 +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + +EOF = b"\x1a" +MAX_BYTES_WRITTEN = 32767 + + +class Py_buffer(ctypes.Structure): + _fields_ = [ + ("buf", c_void_p), + ("obj", py_object), + ("len", c_ssize_t), + ("itemsize", c_ssize_t), + ("readonly", c_int), + ("ndim", c_int), + ("format", c_char_p), + ("shape", c_ssize_p), + ("strides", c_ssize_p), + ("suboffsets", c_ssize_p), + ("internal", c_void_p), + ] + + if PY2: + _fields_.insert(-1, ("smalltable", c_ssize_t * 2)) + + +# On PyPy we cannot get buffers so our ability to operate here is +# serverly limited. +if pythonapi is None: + get_buffer = None +else: + + def get_buffer(obj, writable=False): + buf = Py_buffer() + flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE + PyObject_GetBuffer(py_object(obj), byref(buf), flags) + try: + buffer_type = c_char * buf.len + return buffer_type.from_address(buf.buf) + finally: + PyBuffer_Release(byref(buf)) + + +class _WindowsConsoleRawIOBase(io.RawIOBase): + def __init__(self, handle): + self.handle = handle + + def isatty(self): + io.RawIOBase.isatty(self) + return True + + +class _WindowsConsoleReader(_WindowsConsoleRawIOBase): + def readable(self): + return True + + def readinto(self, b): + bytes_to_be_read = len(b) + if not bytes_to_be_read: + return 0 + elif bytes_to_be_read % 2: + raise ValueError( + "cannot read odd number of bytes from UTF-16-LE encoded console" + ) + + buffer = get_buffer(b, writable=True) + code_units_to_be_read = bytes_to_be_read // 2 + code_units_read = c_ulong() + + rv = ReadConsoleW( + HANDLE(self.handle), + buffer, + code_units_to_be_read, + byref(code_units_read), + None, + ) + if GetLastError() == ERROR_OPERATION_ABORTED: + # wait for KeyboardInterrupt + time.sleep(0.1) + if not rv: + raise OSError("Windows error: {}".format(GetLastError())) + + if buffer[0] == EOF: + return 0 + return 2 * code_units_read.value + + +class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): + def writable(self): + return True + + @staticmethod + def _get_error_message(errno): + if errno == ERROR_SUCCESS: + return "ERROR_SUCCESS" + elif errno == ERROR_NOT_ENOUGH_MEMORY: + return "ERROR_NOT_ENOUGH_MEMORY" + return "Windows error {}".format(errno) + + def write(self, b): + bytes_to_be_written = len(b) + buf = get_buffer(b) + code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 + code_units_written = c_ulong() + + WriteConsoleW( + HANDLE(self.handle), + buf, + code_units_to_be_written, + byref(code_units_written), + None, + ) + bytes_written = 2 * code_units_written.value + + if bytes_written == 0 and bytes_to_be_written > 0: + raise OSError(self._get_error_message(GetLastError())) + return bytes_written + + +class ConsoleStream(object): + def __init__(self, text_stream, byte_stream): + self._text_stream = text_stream + self.buffer = byte_stream + + @property + def name(self): + return self.buffer.name + + def write(self, x): + if isinstance(x, text_type): + return self._text_stream.write(x) + try: + self.flush() + except Exception: + pass + return self.buffer.write(x) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def __getattr__(self, name): + return getattr(self._text_stream, name) + + def isatty(self): + return self.buffer.isatty() + + def __repr__(self): + return "".format( + self.name, self.encoding + ) + + +class WindowsChunkedWriter(object): + """ + Wraps a stream (such as stdout), acting as a transparent proxy for all + attribute access apart from method 'write()' which we wrap to write in + limited chunks due to a Windows limitation on binary console streams. + """ + + def __init__(self, wrapped): + # double-underscore everything to prevent clashes with names of + # attributes on the wrapped stream object. + self.__wrapped = wrapped + + def __getattr__(self, name): + return getattr(self.__wrapped, name) + + def write(self, text): + total_to_write = len(text) + written = 0 + + while written < total_to_write: + to_write = min(total_to_write - written, MAX_BYTES_WRITTEN) + self.__wrapped.write(text[written : written + to_write]) + written += to_write + + +_wrapped_std_streams = set() + + +def _wrap_std_stream(name): + # Python 2 & Windows 7 and below + if ( + PY2 + and sys.getwindowsversion()[:2] <= (6, 1) + and name not in _wrapped_std_streams + ): + setattr(sys, name, WindowsChunkedWriter(getattr(sys, name))) + _wrapped_std_streams.add(name) + + +def _get_text_stdin(buffer_stream): + text_stream = _NonClosingTextIOWrapper( + io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return ConsoleStream(text_stream, buffer_stream) + + +def _get_text_stdout(buffer_stream): + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return ConsoleStream(text_stream, buffer_stream) + + +def _get_text_stderr(buffer_stream): + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return ConsoleStream(text_stream, buffer_stream) + + +if PY2: + + def _hash_py_argv(): + return zlib.crc32("\x00".join(sys.argv[1:])) + + _initial_argv_hash = _hash_py_argv() + + def _get_windows_argv(): + argc = c_int(0) + argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc)) + if not argv_unicode: + raise WinError() + try: + argv = [argv_unicode[i] for i in range(0, argc.value)] + finally: + LocalFree(argv_unicode) + del argv_unicode + + if not hasattr(sys, "frozen"): + argv = argv[1:] + while len(argv) > 0: + arg = argv[0] + if not arg.startswith("-") or arg == "-": + break + argv = argv[1:] + if arg.startswith(("-c", "-m")): + break + + return argv[1:] + + +_stream_factories = { + 0: _get_text_stdin, + 1: _get_text_stdout, + 2: _get_text_stderr, +} + + +def _is_console(f): + if not hasattr(f, "fileno"): + return False + + try: + fileno = f.fileno() + except OSError: + return False + + handle = msvcrt.get_osfhandle(fileno) + return bool(GetConsoleMode(handle, byref(DWORD()))) + + +def _get_windows_console_stream(f, encoding, errors): + if ( + get_buffer is not None + and encoding in ("utf-16-le", None) + and errors in ("strict", None) + and _is_console(f) + ): + func = _stream_factories.get(f.fileno()) + if func is not None: + if not PY2: + f = getattr(f, "buffer", None) + if f is None: + return None + else: + # If we are on Python 2 we need to set the stream that we + # deal with to binary mode as otherwise the exercise if a + # bit moot. The same problems apply as for + # get_binary_stdin and friends from _compat. + msvcrt.setmode(f.fileno(), os.O_BINARY) + return func(f) diff --git a/openpype/vendor/python/python_2/click/core.py b/openpype/vendor/python/python_2/click/core.py new file mode 100644 index 0000000000..f58bf26d2f --- /dev/null +++ b/openpype/vendor/python/python_2/click/core.py @@ -0,0 +1,2030 @@ +import errno +import inspect +import os +import sys +from contextlib import contextmanager +from functools import update_wrapper +from itertools import repeat + +from ._compat import isidentifier +from ._compat import iteritems +from ._compat import PY2 +from ._compat import string_types +from ._unicodefun import _check_for_unicode_literals +from ._unicodefun import _verify_python3_env +from .exceptions import Abort +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import Exit +from .exceptions import MissingParameter +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import join_options +from .globals import pop_context +from .globals import push_context +from .parser import OptionParser +from .parser import split_opt +from .termui import confirm +from .termui import prompt +from .termui import style +from .types import BOOL +from .types import convert_type +from .types import IntRange +from .utils import echo +from .utils import get_os_args +from .utils import make_default_short_help +from .utils import make_str +from .utils import PacifyFlushWrapper + +_missing = object() + +SUBCOMMAND_METAVAR = "COMMAND [ARGS]..." +SUBCOMMANDS_METAVAR = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." + +DEPRECATED_HELP_NOTICE = " (DEPRECATED)" +DEPRECATED_INVOKE_NOTICE = "DeprecationWarning: The command %(name)s is deprecated." + + +def _maybe_show_deprecated_notice(cmd): + if cmd.deprecated: + echo(style(DEPRECATED_INVOKE_NOTICE % {"name": cmd.name}, fg="red"), err=True) + + +def fast_exit(code): + """Exit without garbage collection, this speeds up exit by about 10ms for + things like bash completion. + """ + sys.stdout.flush() + sys.stderr.flush() + os._exit(code) + + +def _bashcomplete(cmd, prog_name, complete_var=None): + """Internal handler for the bash completion support.""" + if complete_var is None: + complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper()) + complete_instr = os.environ.get(complete_var) + if not complete_instr: + return + + from ._bashcomplete import bashcomplete + + if bashcomplete(cmd, prog_name, complete_var, complete_instr): + fast_exit(1) + + +def _check_multicommand(base_command, cmd_name, cmd, register=False): + if not base_command.chain or not isinstance(cmd, MultiCommand): + return + if register: + hint = ( + "It is not possible to add multi commands as children to" + " another multi command that is in chain mode." + ) + else: + hint = ( + "Found a multi command as subcommand to a multi command" + " that is in chain mode. This is not supported." + ) + raise RuntimeError( + "{}. Command '{}' is set to chain and '{}' was added as" + " subcommand but it in itself is a multi command. ('{}' is a {}" + " within a chained {} named '{}').".format( + hint, + base_command.name, + cmd_name, + cmd_name, + cmd.__class__.__name__, + base_command.__class__.__name__, + base_command.name, + ) + ) + + +def batch(iterable, batch_size): + return list(zip(*repeat(iter(iterable), batch_size))) + + +def invoke_param_callback(callback, ctx, param, value): + code = getattr(callback, "__code__", None) + args = getattr(code, "co_argcount", 3) + + if args < 3: + from warnings import warn + + warn( + "Parameter callbacks take 3 args, (ctx, param, value). The" + " 2-arg style is deprecated and will be removed in 8.0.".format(callback), + DeprecationWarning, + stacklevel=3, + ) + return callback(ctx, value) + + return callback(ctx, param, value) + + +@contextmanager +def augment_usage_errors(ctx, param=None): + """Context manager that attaches extra information to exceptions.""" + try: + yield + except BadParameter as e: + if e.ctx is None: + e.ctx = ctx + if param is not None and e.param is None: + e.param = param + raise + except UsageError as e: + if e.ctx is None: + e.ctx = ctx + raise + + +def iter_params_for_processing(invocation_order, declaration_order): + """Given a sequence of parameters in the order as should be considered + for processing and an iterable of parameters that exist, this returns + a list in the correct order as they should be processed. + """ + + def sort_key(item): + try: + idx = invocation_order.index(item) + except ValueError: + idx = float("inf") + return (not item.is_eager, idx) + + return sorted(declaration_order, key=sort_key) + + +class Context(object): + """The context is a special internal object that holds state relevant + for the script execution at every single level. It's normally invisible + to commands unless they opt-in to getting access to it. + + The context is useful as it can pass internal objects around and can + control special execution features such as reading data from + environment variables. + + A context can be used as context manager in which case it will call + :meth:`close` on teardown. + + .. versionadded:: 2.0 + Added the `resilient_parsing`, `help_option_names`, + `token_normalize_func` parameters. + + .. versionadded:: 3.0 + Added the `allow_extra_args` and `allow_interspersed_args` + parameters. + + .. versionadded:: 4.0 + Added the `color`, `ignore_unknown_options`, and + `max_content_width` parameters. + + .. versionadded:: 7.1 + Added the `show_default` parameter. + + :param command: the command class for this context. + :param parent: the parent context. + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it is usually + the name of the script, for commands below it it's + the name of the script. + :param obj: an arbitrary object of user data. + :param auto_envvar_prefix: the prefix to use for automatic environment + variables. If this is `None` then reading + from environment variables is disabled. This + does not affect manually set environment + variables which are always read. + :param default_map: a dictionary (like object) with default values + for parameters. + :param terminal_width: the width of the terminal. The default is + inherit from parent context. If no context + defines the terminal width then auto + detection will be applied. + :param max_content_width: the maximum width for content rendered by + Click (this currently only affects help + pages). This defaults to 80 characters if + not overridden. In other words: even if the + terminal is larger than that, Click will not + format things wider than 80 characters by + default. In addition to that, formatters might + add some safety mapping on the right. + :param resilient_parsing: if this flag is enabled then Click will + parse without any interactivity or callback + invocation. Default values will also be + ignored. This is useful for implementing + things such as completion support. + :param allow_extra_args: if this is set to `True` then extra arguments + at the end will not raise an error and will be + kept on the context. The default is to inherit + from the command. + :param allow_interspersed_args: if this is set to `False` then options + and arguments cannot be mixed. The + default is to inherit from the command. + :param ignore_unknown_options: instructs click to ignore options it does + not know and keeps them for later + processing. + :param help_option_names: optionally a list of strings that define how + the default help parameter is named. The + default is ``['--help']``. + :param token_normalize_func: an optional function that is used to + normalize tokens (options, choices, + etc.). This for instance can be used to + implement case insensitive behavior. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are used in texts that Click prints which is by + default not the case. This for instance would affect + help output. + :param show_default: if True, shows defaults for all options. + Even if an option is later created with show_default=False, + this command-level setting overrides it. + """ + + def __init__( + self, + command, + parent=None, + info_name=None, + obj=None, + auto_envvar_prefix=None, + default_map=None, + terminal_width=None, + max_content_width=None, + resilient_parsing=False, + allow_extra_args=None, + allow_interspersed_args=None, + ignore_unknown_options=None, + help_option_names=None, + token_normalize_func=None, + color=None, + show_default=None, + ): + #: the parent context or `None` if none exists. + self.parent = parent + #: the :class:`Command` for this context. + self.command = command + #: the descriptive information name + self.info_name = info_name + #: the parsed parameters except if the value is hidden in which + #: case it's not remembered. + self.params = {} + #: the leftover arguments. + self.args = [] + #: protected arguments. These are arguments that are prepended + #: to `args` when certain parsing scenarios are encountered but + #: must be never propagated to another arguments. This is used + #: to implement nested parsing. + self.protected_args = [] + if obj is None and parent is not None: + obj = parent.obj + #: the user object stored. + self.obj = obj + self._meta = getattr(parent, "meta", {}) + + #: A dictionary (-like object) with defaults for parameters. + if ( + default_map is None + and parent is not None + and parent.default_map is not None + ): + default_map = parent.default_map.get(info_name) + self.default_map = default_map + + #: This flag indicates if a subcommand is going to be executed. A + #: group callback can use this information to figure out if it's + #: being executed directly or because the execution flow passes + #: onwards to a subcommand. By default it's None, but it can be + #: the name of the subcommand to execute. + #: + #: If chaining is enabled this will be set to ``'*'`` in case + #: any commands are executed. It is however not possible to + #: figure out which ones. If you require this knowledge you + #: should use a :func:`resultcallback`. + self.invoked_subcommand = None + + if terminal_width is None and parent is not None: + terminal_width = parent.terminal_width + #: The width of the terminal (None is autodetection). + self.terminal_width = terminal_width + + if max_content_width is None and parent is not None: + max_content_width = parent.max_content_width + #: The maximum width of formatted content (None implies a sensible + #: default which is 80 for most things). + self.max_content_width = max_content_width + + if allow_extra_args is None: + allow_extra_args = command.allow_extra_args + #: Indicates if the context allows extra args or if it should + #: fail on parsing. + #: + #: .. versionadded:: 3.0 + self.allow_extra_args = allow_extra_args + + if allow_interspersed_args is None: + allow_interspersed_args = command.allow_interspersed_args + #: Indicates if the context allows mixing of arguments and + #: options or not. + #: + #: .. versionadded:: 3.0 + self.allow_interspersed_args = allow_interspersed_args + + if ignore_unknown_options is None: + ignore_unknown_options = command.ignore_unknown_options + #: Instructs click to ignore options that a command does not + #: understand and will store it on the context for later + #: processing. This is primarily useful for situations where you + #: want to call into external programs. Generally this pattern is + #: strongly discouraged because it's not possibly to losslessly + #: forward all arguments. + #: + #: .. versionadded:: 4.0 + self.ignore_unknown_options = ignore_unknown_options + + if help_option_names is None: + if parent is not None: + help_option_names = parent.help_option_names + else: + help_option_names = ["--help"] + + #: The names for the help options. + self.help_option_names = help_option_names + + if token_normalize_func is None and parent is not None: + token_normalize_func = parent.token_normalize_func + + #: An optional normalization function for tokens. This is + #: options, choices, commands etc. + self.token_normalize_func = token_normalize_func + + #: Indicates if resilient parsing is enabled. In that case Click + #: will do its best to not cause any failures and default values + #: will be ignored. Useful for completion. + self.resilient_parsing = resilient_parsing + + # If there is no envvar prefix yet, but the parent has one and + # the command on this level has a name, we can expand the envvar + # prefix automatically. + if auto_envvar_prefix is None: + if ( + parent is not None + and parent.auto_envvar_prefix is not None + and self.info_name is not None + ): + auto_envvar_prefix = "{}_{}".format( + parent.auto_envvar_prefix, self.info_name.upper() + ) + else: + auto_envvar_prefix = auto_envvar_prefix.upper() + if auto_envvar_prefix is not None: + auto_envvar_prefix = auto_envvar_prefix.replace("-", "_") + self.auto_envvar_prefix = auto_envvar_prefix + + if color is None and parent is not None: + color = parent.color + + #: Controls if styling output is wanted or not. + self.color = color + + self.show_default = show_default + + self._close_callbacks = [] + self._depth = 0 + + def __enter__(self): + self._depth += 1 + push_context(self) + return self + + def __exit__(self, exc_type, exc_value, tb): + self._depth -= 1 + if self._depth == 0: + self.close() + pop_context() + + @contextmanager + def scope(self, cleanup=True): + """This helper method can be used with the context object to promote + it to the current thread local (see :func:`get_current_context`). + The default behavior of this is to invoke the cleanup functions which + can be disabled by setting `cleanup` to `False`. The cleanup + functions are typically used for things such as closing file handles. + + If the cleanup is intended the context object can also be directly + used as a context manager. + + Example usage:: + + with ctx.scope(): + assert get_current_context() is ctx + + This is equivalent:: + + with ctx: + assert get_current_context() is ctx + + .. versionadded:: 5.0 + + :param cleanup: controls if the cleanup functions should be run or + not. The default is to run these functions. In + some situations the context only wants to be + temporarily pushed in which case this can be disabled. + Nested pushes automatically defer the cleanup. + """ + if not cleanup: + self._depth += 1 + try: + with self as rv: + yield rv + finally: + if not cleanup: + self._depth -= 1 + + @property + def meta(self): + """This is a dictionary which is shared with all the contexts + that are nested. It exists so that click utilities can store some + state here if they need to. It is however the responsibility of + that code to manage this dictionary well. + + The keys are supposed to be unique dotted strings. For instance + module paths are a good choice for it. What is stored in there is + irrelevant for the operation of click. However what is important is + that code that places data here adheres to the general semantics of + the system. + + Example usage:: + + LANG_KEY = f'{__name__}.lang' + + def set_language(value): + ctx = get_current_context() + ctx.meta[LANG_KEY] = value + + def get_language(): + return get_current_context().meta.get(LANG_KEY, 'en_US') + + .. versionadded:: 5.0 + """ + return self._meta + + def make_formatter(self): + """Creates the formatter for the help and usage output.""" + return HelpFormatter( + width=self.terminal_width, max_width=self.max_content_width + ) + + def call_on_close(self, f): + """This decorator remembers a function as callback that should be + executed when the context tears down. This is most useful to bind + resource handling to the script execution. For instance, file objects + opened by the :class:`File` type will register their close callbacks + here. + + :param f: the function to execute on teardown. + """ + self._close_callbacks.append(f) + return f + + def close(self): + """Invokes all close callbacks.""" + for cb in self._close_callbacks: + cb() + self._close_callbacks = [] + + @property + def command_path(self): + """The computed command path. This is used for the ``usage`` + information on the help page. It's automatically created by + combining the info names of the chain of contexts to the root. + """ + rv = "" + if self.info_name is not None: + rv = self.info_name + if self.parent is not None: + rv = "{} {}".format(self.parent.command_path, rv) + return rv.lstrip() + + def find_root(self): + """Finds the outermost context.""" + node = self + while node.parent is not None: + node = node.parent + return node + + def find_object(self, object_type): + """Finds the closest object of a given type.""" + node = self + while node is not None: + if isinstance(node.obj, object_type): + return node.obj + node = node.parent + + def ensure_object(self, object_type): + """Like :meth:`find_object` but sets the innermost object to a + new instance of `object_type` if it does not exist. + """ + rv = self.find_object(object_type) + if rv is None: + self.obj = rv = object_type() + return rv + + def lookup_default(self, name): + """Looks up the default for a parameter name. This by default + looks into the :attr:`default_map` if available. + """ + if self.default_map is not None: + rv = self.default_map.get(name) + if callable(rv): + rv = rv() + return rv + + def fail(self, message): + """Aborts the execution of the program with a specific error + message. + + :param message: the error message to fail with. + """ + raise UsageError(message, self) + + def abort(self): + """Aborts the script.""" + raise Abort() + + def exit(self, code=0): + """Exits the application with a given exit code.""" + raise Exit(code) + + def get_usage(self): + """Helper method to get formatted usage string for the current + context and command. + """ + return self.command.get_usage(self) + + def get_help(self): + """Helper method to get formatted help page for the current + context and command. + """ + return self.command.get_help(self) + + def invoke(*args, **kwargs): # noqa: B902 + """Invokes a command callback in exactly the way it expects. There + are two ways to invoke this method: + + 1. the first argument can be a callback and all other arguments and + keyword arguments are forwarded directly to the function. + 2. the first argument is a click command object. In that case all + arguments are forwarded as well but proper click parameters + (options and click arguments) must be keyword arguments and Click + will fill in defaults. + + Note that before Click 3.2 keyword arguments were not properly filled + in against the intention of this code and no context was created. For + more information about this change and why it was done in a bugfix + release see :ref:`upgrade-to-3.2`. + """ + self, callback = args[:2] + ctx = self + + # It's also possible to invoke another command which might or + # might not have a callback. In that case we also fill + # in defaults and make a new context for this command. + if isinstance(callback, Command): + other_cmd = callback + callback = other_cmd.callback + ctx = Context(other_cmd, info_name=other_cmd.name, parent=self) + if callback is None: + raise TypeError( + "The given command does not have a callback that can be invoked." + ) + + for param in other_cmd.params: + if param.name not in kwargs and param.expose_value: + kwargs[param.name] = param.get_default(ctx) + + args = args[2:] + with augment_usage_errors(self): + with ctx: + return callback(*args, **kwargs) + + def forward(*args, **kwargs): # noqa: B902 + """Similar to :meth:`invoke` but fills in default keyword + arguments from the current context if the other command expects + it. This cannot invoke callbacks directly, only other commands. + """ + self, cmd = args[:2] + + # It's also possible to invoke another command which might or + # might not have a callback. + if not isinstance(cmd, Command): + raise TypeError("Callback is not a command.") + + for param in self.params: + if param not in kwargs: + kwargs[param] = self.params[param] + + return self.invoke(cmd, **kwargs) + + +class BaseCommand(object): + """The base command implements the minimal API contract of commands. + Most code will never use this as it does not implement a lot of useful + functionality but it can act as the direct subclass of alternative + parsing methods that do not depend on the Click parser. + + For instance, this can be used to bridge Click and other systems like + argparse or docopt. + + Because base commands do not implement a lot of the API that other + parts of Click take for granted, they are not supported for all + operations. For instance, they cannot be used with the decorators + usually and they have no built-in callback system. + + .. versionchanged:: 2.0 + Added the `context_settings` parameter. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + """ + + #: the default for the :attr:`Context.allow_extra_args` flag. + allow_extra_args = False + #: the default for the :attr:`Context.allow_interspersed_args` flag. + allow_interspersed_args = True + #: the default for the :attr:`Context.ignore_unknown_options` flag. + ignore_unknown_options = False + + def __init__(self, name, context_settings=None): + #: the name the command thinks it has. Upon registering a command + #: on a :class:`Group` the group will default the command name + #: with this information. You should instead use the + #: :class:`Context`\'s :attr:`~Context.info_name` attribute. + self.name = name + if context_settings is None: + context_settings = {} + #: an optional dictionary with defaults passed to the context. + self.context_settings = context_settings + + def __repr__(self): + return "<{} {}>".format(self.__class__.__name__, self.name) + + def get_usage(self, ctx): + raise NotImplementedError("Base commands cannot get usage") + + def get_help(self, ctx): + raise NotImplementedError("Base commands cannot get help") + + def make_context(self, info_name, args, parent=None, **extra): + """This function when given an info name and arguments will kick + off the parsing and create a new :class:`Context`. It does not + invoke the actual command callback though. + + :param info_name: the info name for this invokation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it's usually + the name of the script, for commands below it it's + the name of the script. + :param args: the arguments to parse as list of strings. + :param parent: the parent context if available. + :param extra: extra keyword arguments forwarded to the context + constructor. + """ + for key, value in iteritems(self.context_settings): + if key not in extra: + extra[key] = value + ctx = Context(self, info_name=info_name, parent=parent, **extra) + with ctx.scope(cleanup=False): + self.parse_args(ctx, args) + return ctx + + def parse_args(self, ctx, args): + """Given a context and a list of arguments this creates the parser + and parses the arguments, then modifies the context as necessary. + This is automatically invoked by :meth:`make_context`. + """ + raise NotImplementedError("Base commands do not know how to parse arguments.") + + def invoke(self, ctx): + """Given a context, this invokes the command. The default + implementation is raising a not implemented error. + """ + raise NotImplementedError("Base commands are not invokable by default") + + def main( + self, + args=None, + prog_name=None, + complete_var=None, + standalone_mode=True, + **extra + ): + """This is the way to invoke a script with all the bells and + whistles as a command line application. This will always terminate + the application after a call. If this is not wanted, ``SystemExit`` + needs to be caught. + + This method is also available by directly calling the instance of + a :class:`Command`. + + .. versionadded:: 3.0 + Added the `standalone_mode` flag to control the standalone mode. + + :param args: the arguments that should be used for parsing. If not + provided, ``sys.argv[1:]`` is used. + :param prog_name: the program name that should be used. By default + the program name is constructed by taking the file + name from ``sys.argv[0]``. + :param complete_var: the environment variable that controls the + bash completion support. The default is + ``"__COMPLETE"`` with prog_name in + uppercase. + :param standalone_mode: the default behavior is to invoke the script + in standalone mode. Click will then + handle exceptions and convert them into + error messages and the function will never + return but shut down the interpreter. If + this is set to `False` they will be + propagated to the caller and the return + value of this function is the return value + of :meth:`invoke`. + :param extra: extra keyword arguments are forwarded to the context + constructor. See :class:`Context` for more information. + """ + # If we are in Python 3, we will verify that the environment is + # sane at this point or reject further execution to avoid a + # broken script. + if not PY2: + _verify_python3_env() + else: + _check_for_unicode_literals() + + if args is None: + args = get_os_args() + else: + args = list(args) + + if prog_name is None: + prog_name = make_str( + os.path.basename(sys.argv[0] if sys.argv else __file__) + ) + + # Hook for the Bash completion. This only activates if the Bash + # completion is actually enabled, otherwise this is quite a fast + # noop. + _bashcomplete(self, prog_name, complete_var) + + try: + try: + with self.make_context(prog_name, args, **extra) as ctx: + rv = self.invoke(ctx) + if not standalone_mode: + return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness + ctx.exit() + except (EOFError, KeyboardInterrupt): + echo(file=sys.stderr) + raise Abort() + except ClickException as e: + if not standalone_mode: + raise + e.show() + sys.exit(e.exit_code) + except IOError as e: + if e.errno == errno.EPIPE: + sys.stdout = PacifyFlushWrapper(sys.stdout) + sys.stderr = PacifyFlushWrapper(sys.stderr) + sys.exit(1) + else: + raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code + except Abort: + if not standalone_mode: + raise + echo("Aborted!", file=sys.stderr) + sys.exit(1) + + def __call__(self, *args, **kwargs): + """Alias for :meth:`main`.""" + return self.main(*args, **kwargs) + + +class Command(BaseCommand): + """Commands are the basic building block of command line interfaces in + Click. A basic command handles command line parsing and might dispatch + more parsing to commands nested below it. + + .. versionchanged:: 2.0 + Added the `context_settings` parameter. + .. versionchanged:: 7.1 + Added the `no_args_is_help` parameter. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + :param callback: the callback to invoke. This is optional. + :param params: the parameters to register with this command. This can + be either :class:`Option` or :class:`Argument` objects. + :param help: the help string to use for this command. + :param epilog: like the help string but it's printed at the end of the + help page after everything else. + :param short_help: the short help to use for this command. This is + shown on the command listing of the parent command. + :param add_help_option: by default each command registers a ``--help`` + option. This can be disabled by this parameter. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is disabled by default. + If enabled this will add ``--help`` as argument + if no arguments are passed + :param hidden: hide this command from help outputs. + + :param deprecated: issues a message indicating that + the command is deprecated. + """ + + def __init__( + self, + name, + context_settings=None, + callback=None, + params=None, + help=None, + epilog=None, + short_help=None, + options_metavar="[OPTIONS]", + add_help_option=True, + no_args_is_help=False, + hidden=False, + deprecated=False, + ): + BaseCommand.__init__(self, name, context_settings) + #: the callback to execute when the command fires. This might be + #: `None` in which case nothing happens. + self.callback = callback + #: the list of parameters for this command in the order they + #: should show up in the help page and execute. Eager parameters + #: will automatically be handled before non eager ones. + self.params = params or [] + # if a form feed (page break) is found in the help text, truncate help + # text to the content preceding the first form feed + if help and "\f" in help: + help = help.split("\f", 1)[0] + self.help = help + self.epilog = epilog + self.options_metavar = options_metavar + self.short_help = short_help + self.add_help_option = add_help_option + self.no_args_is_help = no_args_is_help + self.hidden = hidden + self.deprecated = deprecated + + def get_usage(self, ctx): + """Formats the usage line into a string and returns it. + + Calls :meth:`format_usage` internally. + """ + formatter = ctx.make_formatter() + self.format_usage(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_params(self, ctx): + rv = self.params + help_option = self.get_help_option(ctx) + if help_option is not None: + rv = rv + [help_option] + return rv + + def format_usage(self, ctx, formatter): + """Writes the usage line into the formatter. + + This is a low-level method called by :meth:`get_usage`. + """ + pieces = self.collect_usage_pieces(ctx) + formatter.write_usage(ctx.command_path, " ".join(pieces)) + + def collect_usage_pieces(self, ctx): + """Returns all the pieces that go into the usage line and returns + it as a list of strings. + """ + rv = [self.options_metavar] + for param in self.get_params(ctx): + rv.extend(param.get_usage_pieces(ctx)) + return rv + + def get_help_option_names(self, ctx): + """Returns the names for the help option.""" + all_names = set(ctx.help_option_names) + for param in self.params: + all_names.difference_update(param.opts) + all_names.difference_update(param.secondary_opts) + return all_names + + def get_help_option(self, ctx): + """Returns the help option object.""" + help_options = self.get_help_option_names(ctx) + if not help_options or not self.add_help_option: + return + + def show_help(ctx, param, value): + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + return Option( + help_options, + is_flag=True, + is_eager=True, + expose_value=False, + callback=show_help, + help="Show this message and exit.", + ) + + def make_parser(self, ctx): + """Creates the underlying option parser for this command.""" + parser = OptionParser(ctx) + for param in self.get_params(ctx): + param.add_to_parser(parser, ctx) + return parser + + def get_help(self, ctx): + """Formats the help into a string and returns it. + + Calls :meth:`format_help` internally. + """ + formatter = ctx.make_formatter() + self.format_help(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_short_help_str(self, limit=45): + """Gets short help for the command or makes it by shortening the + long help string. + """ + return ( + self.short_help + or self.help + and make_default_short_help(self.help, limit) + or "" + ) + + def format_help(self, ctx, formatter): + """Writes the help into the formatter if it exists. + + This is a low-level method called by :meth:`get_help`. + + This calls the following methods: + + - :meth:`format_usage` + - :meth:`format_help_text` + - :meth:`format_options` + - :meth:`format_epilog` + """ + self.format_usage(ctx, formatter) + self.format_help_text(ctx, formatter) + self.format_options(ctx, formatter) + self.format_epilog(ctx, formatter) + + def format_help_text(self, ctx, formatter): + """Writes the help text to the formatter if it exists.""" + if self.help: + formatter.write_paragraph() + with formatter.indentation(): + help_text = self.help + if self.deprecated: + help_text += DEPRECATED_HELP_NOTICE + formatter.write_text(help_text) + elif self.deprecated: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(DEPRECATED_HELP_NOTICE) + + def format_options(self, ctx, formatter): + """Writes all the options into the formatter if they exist.""" + opts = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None: + opts.append(rv) + + if opts: + with formatter.section("Options"): + formatter.write_dl(opts) + + def format_epilog(self, ctx, formatter): + """Writes the epilog into the formatter if it exists.""" + if self.epilog: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(self.epilog) + + def parse_args(self, ctx, args): + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + parser = self.make_parser(ctx) + opts, args, param_order = parser.parse_args(args=args) + + for param in iter_params_for_processing(param_order, self.get_params(ctx)): + value, args = param.handle_parse_result(ctx, opts, args) + + if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + ctx.fail( + "Got unexpected extra argument{} ({})".format( + "s" if len(args) != 1 else "", " ".join(map(make_str, args)) + ) + ) + + ctx.args = args + return args + + def invoke(self, ctx): + """Given a context, this invokes the attached callback (if it exists) + in the right way. + """ + _maybe_show_deprecated_notice(self) + if self.callback is not None: + return ctx.invoke(self.callback, **ctx.params) + + +class MultiCommand(Command): + """A multi command is the basic implementation of a command that + dispatches to subcommands. The most common version is the + :class:`Group`. + + :param invoke_without_command: this controls how the multi command itself + is invoked. By default it's only invoked + if a subcommand is provided. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is enabled by default if + `invoke_without_command` is disabled or disabled + if it's enabled. If enabled this will add + ``--help`` as argument if no arguments are + passed. + :param subcommand_metavar: the string that is used in the documentation + to indicate the subcommand place. + :param chain: if this is set to `True` chaining of multiple subcommands + is enabled. This restricts the form of commands in that + they cannot have optional arguments but it allows + multiple commands to be chained together. + :param result_callback: the result callback to attach to this multi + command. + """ + + allow_extra_args = True + allow_interspersed_args = False + + def __init__( + self, + name=None, + invoke_without_command=False, + no_args_is_help=None, + subcommand_metavar=None, + chain=False, + result_callback=None, + **attrs + ): + Command.__init__(self, name, **attrs) + if no_args_is_help is None: + no_args_is_help = not invoke_without_command + self.no_args_is_help = no_args_is_help + self.invoke_without_command = invoke_without_command + if subcommand_metavar is None: + if chain: + subcommand_metavar = SUBCOMMANDS_METAVAR + else: + subcommand_metavar = SUBCOMMAND_METAVAR + self.subcommand_metavar = subcommand_metavar + self.chain = chain + #: The result callback that is stored. This can be set or + #: overridden with the :func:`resultcallback` decorator. + self.result_callback = result_callback + + if self.chain: + for param in self.params: + if isinstance(param, Argument) and not param.required: + raise RuntimeError( + "Multi commands in chain mode cannot have" + " optional arguments." + ) + + def collect_usage_pieces(self, ctx): + rv = Command.collect_usage_pieces(self, ctx) + rv.append(self.subcommand_metavar) + return rv + + def format_options(self, ctx, formatter): + Command.format_options(self, ctx, formatter) + self.format_commands(ctx, formatter) + + def resultcallback(self, replace=False): + """Adds a result callback to the chain command. By default if a + result callback is already registered this will chain them but + this can be disabled with the `replace` parameter. The result + callback is invoked with the return value of the subcommand + (or the list of return values from all subcommands if chaining + is enabled) as well as the parameters as they would be passed + to the main callback. + + Example:: + + @click.group() + @click.option('-i', '--input', default=23) + def cli(input): + return 42 + + @cli.resultcallback() + def process_result(result, input): + return result + input + + .. versionadded:: 3.0 + + :param replace: if set to `True` an already existing result + callback will be removed. + """ + + def decorator(f): + old_callback = self.result_callback + if old_callback is None or replace: + self.result_callback = f + return f + + def function(__value, *args, **kwargs): + return f(old_callback(__value, *args, **kwargs), *args, **kwargs) + + self.result_callback = rv = update_wrapper(function, f) + return rv + + return decorator + + def format_commands(self, ctx, formatter): + """Extra format methods for multi methods that adds all the commands + after the options. + """ + commands = [] + for subcommand in self.list_commands(ctx): + cmd = self.get_command(ctx, subcommand) + # What is this, the tool lied about a command. Ignore it + if cmd is None: + continue + if cmd.hidden: + continue + + commands.append((subcommand, cmd)) + + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section("Commands"): + formatter.write_dl(rows) + + def parse_args(self, ctx, args): + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + rest = Command.parse_args(self, ctx, args) + if self.chain: + ctx.protected_args = rest + ctx.args = [] + elif rest: + ctx.protected_args, ctx.args = rest[:1], rest[1:] + + return ctx.args + + def invoke(self, ctx): + def _process_result(value): + if self.result_callback is not None: + value = ctx.invoke(self.result_callback, value, **ctx.params) + return value + + if not ctx.protected_args: + # If we are invoked without command the chain flag controls + # how this happens. If we are not in chain mode, the return + # value here is the return value of the command. + # If however we are in chain mode, the return value is the + # return value of the result processor invoked with an empty + # list (which means that no subcommand actually was executed). + if self.invoke_without_command: + if not self.chain: + return Command.invoke(self, ctx) + with ctx: + Command.invoke(self, ctx) + return _process_result([]) + ctx.fail("Missing command.") + + # Fetch args back out + args = ctx.protected_args + ctx.args + ctx.args = [] + ctx.protected_args = [] + + # If we're not in chain mode, we only allow the invocation of a + # single command but we also inform the current context about the + # name of the command to invoke. + if not self.chain: + # Make sure the context is entered so we do not clean up + # resources until the result processor has worked. + with ctx: + cmd_name, cmd, args = self.resolve_command(ctx, args) + ctx.invoked_subcommand = cmd_name + Command.invoke(self, ctx) + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) + with sub_ctx: + return _process_result(sub_ctx.command.invoke(sub_ctx)) + + # In chain mode we create the contexts step by step, but after the + # base command has been invoked. Because at that point we do not + # know the subcommands yet, the invoked subcommand attribute is + # set to ``*`` to inform the command that subcommands are executed + # but nothing else. + with ctx: + ctx.invoked_subcommand = "*" if args else None + Command.invoke(self, ctx) + + # Otherwise we make every single context and invoke them in a + # chain. In that case the return value to the result processor + # is the list of all invoked subcommand's results. + contexts = [] + while args: + cmd_name, cmd, args = self.resolve_command(ctx, args) + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + ) + contexts.append(sub_ctx) + args, sub_ctx.args = sub_ctx.args, [] + + rv = [] + for sub_ctx in contexts: + with sub_ctx: + rv.append(sub_ctx.command.invoke(sub_ctx)) + return _process_result(rv) + + def resolve_command(self, ctx, args): + cmd_name = make_str(args[0]) + original_cmd_name = cmd_name + + # Get the command + cmd = self.get_command(ctx, cmd_name) + + # If we can't find the command but there is a normalization + # function available, we try with that one. + if cmd is None and ctx.token_normalize_func is not None: + cmd_name = ctx.token_normalize_func(cmd_name) + cmd = self.get_command(ctx, cmd_name) + + # If we don't find the command we want to show an error message + # to the user that it was not provided. However, there is + # something else we should do: if the first argument looks like + # an option we want to kick off parsing again for arguments to + # resolve things like --help which now should go to the main + # place. + if cmd is None and not ctx.resilient_parsing: + if split_opt(cmd_name)[0]: + self.parse_args(ctx, ctx.args) + ctx.fail("No such command '{}'.".format(original_cmd_name)) + + return cmd_name, cmd, args[1:] + + def get_command(self, ctx, cmd_name): + """Given a context and a command name, this returns a + :class:`Command` object if it exists or returns `None`. + """ + raise NotImplementedError() + + def list_commands(self, ctx): + """Returns a list of subcommand names in the order they should + appear. + """ + return [] + + +class Group(MultiCommand): + """A group allows a command to have subcommands attached. This is the + most common way to implement nesting in Click. + + :param commands: a dictionary of commands. + """ + + def __init__(self, name=None, commands=None, **attrs): + MultiCommand.__init__(self, name, **attrs) + #: the registered subcommands by their exported names. + self.commands = commands or {} + + def add_command(self, cmd, name=None): + """Registers another :class:`Command` with this group. If the name + is not provided, the name of the command is used. + """ + name = name or cmd.name + if name is None: + raise TypeError("Command has no name.") + _check_multicommand(self, name, cmd, register=True) + self.commands[name] = cmd + + def command(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + """ + from .decorators import command + + def decorator(f): + cmd = command(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + return decorator + + def group(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + """ + from .decorators import group + + def decorator(f): + cmd = group(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + return decorator + + def get_command(self, ctx, cmd_name): + return self.commands.get(cmd_name) + + def list_commands(self, ctx): + return sorted(self.commands) + + +class CommandCollection(MultiCommand): + """A command collection is a multi command that merges multiple multi + commands together into one. This is a straightforward implementation + that accepts a list of different multi commands as sources and + provides all the commands for each of them. + """ + + def __init__(self, name=None, sources=None, **attrs): + MultiCommand.__init__(self, name, **attrs) + #: The list of registered multi commands. + self.sources = sources or [] + + def add_source(self, multi_cmd): + """Adds a new multi command to the chain dispatcher.""" + self.sources.append(multi_cmd) + + def get_command(self, ctx, cmd_name): + for source in self.sources: + rv = source.get_command(ctx, cmd_name) + if rv is not None: + if self.chain: + _check_multicommand(self, cmd_name, rv) + return rv + + def list_commands(self, ctx): + rv = set() + for source in self.sources: + rv.update(source.list_commands(ctx)) + return sorted(rv) + + +class Parameter(object): + r"""A parameter to a command comes in two versions: they are either + :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently + not supported by design as some of the internals for parsing are + intentionally not finalized. + + Some settings are supported by both options and arguments. + + :param param_decls: the parameter declarations for this option or + argument. This is a list of flags or argument + names. + :param type: the type that should be used. Either a :class:`ParamType` + or a Python type. The later is converted into the former + automatically if supported. + :param required: controls if this is optional or not. + :param default: the default value if omitted. This can also be a callable, + in which case it's invoked when the default is needed + without any arguments. + :param callback: a callback that should be executed after the parameter + was matched. This is called as ``fn(ctx, param, + value)`` and needs to return the value. + :param nargs: the number of arguments to match. If not ``1`` the return + value is a tuple instead of single value. The default for + nargs is ``1`` (except if the type is a tuple, then it's + the arity of the tuple). + :param metavar: how the value is represented in the help page. + :param expose_value: if this is `True` then the value is passed onwards + to the command callback and stored on the context, + otherwise it's skipped. + :param is_eager: eager values are processed before non eager ones. This + should not be set for arguments or it will inverse the + order of processing. + :param envvar: a string or list of strings that are environment variables + that should be checked. + + .. versionchanged:: 7.1 + Empty environment variables are ignored rather than taking the + empty string value. This makes it possible for scripts to clear + variables if they can't unset them. + + .. versionchanged:: 2.0 + Changed signature for parameter callback to also be passed the + parameter. The old callback format will still work, but it will + raise a warning to give you a chance to migrate the code easier. + """ + param_type_name = "parameter" + + def __init__( + self, + param_decls=None, + type=None, + required=False, + default=None, + callback=None, + nargs=None, + metavar=None, + expose_value=True, + is_eager=False, + envvar=None, + autocompletion=None, + ): + self.name, self.opts, self.secondary_opts = self._parse_decls( + param_decls or (), expose_value + ) + + self.type = convert_type(type, default) + + # Default nargs to what the type tells us if we have that + # information available. + if nargs is None: + if self.type.is_composite: + nargs = self.type.arity + else: + nargs = 1 + + self.required = required + self.callback = callback + self.nargs = nargs + self.multiple = False + self.expose_value = expose_value + self.default = default + self.is_eager = is_eager + self.metavar = metavar + self.envvar = envvar + self.autocompletion = autocompletion + + def __repr__(self): + return "<{} {}>".format(self.__class__.__name__, self.name) + + @property + def human_readable_name(self): + """Returns the human readable name of this parameter. This is the + same as the name for options, but the metavar for arguments. + """ + return self.name + + def make_metavar(self): + if self.metavar is not None: + return self.metavar + metavar = self.type.get_metavar(self) + if metavar is None: + metavar = self.type.name.upper() + if self.nargs != 1: + metavar += "..." + return metavar + + def get_default(self, ctx): + """Given a context variable this calculates the default value.""" + # Otherwise go with the regular default. + if callable(self.default): + rv = self.default() + else: + rv = self.default + return self.type_cast_value(ctx, rv) + + def add_to_parser(self, parser, ctx): + pass + + def consume_value(self, ctx, opts): + value = opts.get(self.name) + if value is None: + value = self.value_from_envvar(ctx) + if value is None: + value = ctx.lookup_default(self.name) + return value + + def type_cast_value(self, ctx, value): + """Given a value this runs it properly through the type system. + This automatically handles things like `nargs` and `multiple` as + well as composite types. + """ + if self.type.is_composite: + if self.nargs <= 1: + raise TypeError( + "Attempted to invoke composite type but nargs has" + " been set to {}. This is not supported; nargs" + " needs to be set to a fixed value > 1.".format(self.nargs) + ) + if self.multiple: + return tuple(self.type(x or (), self, ctx) for x in value or ()) + return self.type(value or (), self, ctx) + + def _convert(value, level): + if level == 0: + return self.type(value, self, ctx) + return tuple(_convert(x, level - 1) for x in value or ()) + + return _convert(value, (self.nargs != 1) + bool(self.multiple)) + + def process_value(self, ctx, value): + """Given a value and context this runs the logic to convert the + value as necessary. + """ + # If the value we were given is None we do nothing. This way + # code that calls this can easily figure out if something was + # not provided. Otherwise it would be converted into an empty + # tuple for multiple invocations which is inconvenient. + if value is not None: + return self.type_cast_value(ctx, value) + + def value_is_missing(self, value): + if value is None: + return True + if (self.nargs != 1 or self.multiple) and value == (): + return True + return False + + def full_process_value(self, ctx, value): + value = self.process_value(ctx, value) + + if value is None and not ctx.resilient_parsing: + value = self.get_default(ctx) + + if self.required and self.value_is_missing(value): + raise MissingParameter(ctx=ctx, param=self) + + return value + + def resolve_envvar_value(self, ctx): + if self.envvar is None: + return + if isinstance(self.envvar, (tuple, list)): + for envvar in self.envvar: + rv = os.environ.get(envvar) + if rv is not None: + return rv + else: + rv = os.environ.get(self.envvar) + + if rv != "": + return rv + + def value_from_envvar(self, ctx): + rv = self.resolve_envvar_value(ctx) + if rv is not None and self.nargs != 1: + rv = self.type.split_envvar_value(rv) + return rv + + def handle_parse_result(self, ctx, opts, args): + with augment_usage_errors(ctx, param=self): + value = self.consume_value(ctx, opts) + try: + value = self.full_process_value(ctx, value) + except Exception: + if not ctx.resilient_parsing: + raise + value = None + if self.callback is not None: + try: + value = invoke_param_callback(self.callback, ctx, self, value) + except Exception: + if not ctx.resilient_parsing: + raise + + if self.expose_value: + ctx.params[self.name] = value + return value, args + + def get_help_record(self, ctx): + pass + + def get_usage_pieces(self, ctx): + return [] + + def get_error_hint(self, ctx): + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + """ + hint_list = self.opts or [self.human_readable_name] + return " / ".join(repr(x) for x in hint_list) + + +class Option(Parameter): + """Options are usually optional values on the command line and + have some extra features that arguments don't have. + + All other parameters are passed onwards to the parameter constructor. + + :param show_default: controls if the default value should be shown on the + help page. Normally, defaults are not shown. If this + value is a string, it shows the string instead of the + value. This is particularly useful for dynamic options. + :param show_envvar: controls if an environment variable should be shown on + the help page. Normally, environment variables + are not shown. + :param prompt: if set to `True` or a non empty string then the user will be + prompted for input. If set to `True` the prompt will be the + option name capitalized. + :param confirmation_prompt: if set then the value will need to be confirmed + if it was prompted for. + :param hide_input: if this is `True` then the input on the prompt will be + hidden from the user. This is useful for password + input. + :param is_flag: forces this option to act as a flag. The default is + auto detection. + :param flag_value: which value should be used for this flag if it's + enabled. This is set to a boolean automatically if + the option string contains a slash to mark two options. + :param multiple: if this is set to `True` then the argument is accepted + multiple times and recorded. This is similar to ``nargs`` + in how it works but supports arbitrary number of + arguments. + :param count: this flag makes an option increment an integer. + :param allow_from_autoenv: if this is enabled then the value of this + parameter will be pulled from an environment + variable in case a prefix is defined on the + context. + :param help: the help string. + :param hidden: hide this option from help outputs. + """ + + param_type_name = "option" + + def __init__( + self, + param_decls=None, + show_default=False, + prompt=False, + confirmation_prompt=False, + hide_input=False, + is_flag=None, + flag_value=None, + multiple=False, + count=False, + allow_from_autoenv=True, + type=None, + help=None, + hidden=False, + show_choices=True, + show_envvar=False, + **attrs + ): + default_is_missing = attrs.get("default", _missing) is _missing + Parameter.__init__(self, param_decls, type=type, **attrs) + + if prompt is True: + prompt_text = self.name.replace("_", " ").capitalize() + elif prompt is False: + prompt_text = None + else: + prompt_text = prompt + self.prompt = prompt_text + self.confirmation_prompt = confirmation_prompt + self.hide_input = hide_input + self.hidden = hidden + + # Flags + if is_flag is None: + if flag_value is not None: + is_flag = True + else: + is_flag = bool(self.secondary_opts) + if is_flag and default_is_missing: + self.default = False + if flag_value is None: + flag_value = not self.default + self.is_flag = is_flag + self.flag_value = flag_value + if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]: + self.type = BOOL + self.is_bool_flag = True + else: + self.is_bool_flag = False + + # Counting + self.count = count + if count: + if type is None: + self.type = IntRange(min=0) + if default_is_missing: + self.default = 0 + + self.multiple = multiple + self.allow_from_autoenv = allow_from_autoenv + self.help = help + self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar + + # Sanity check for stuff we don't support + if __debug__: + if self.nargs < 0: + raise TypeError("Options cannot have nargs < 0") + if self.prompt and self.is_flag and not self.is_bool_flag: + raise TypeError("Cannot prompt for flags that are not bools.") + if not self.is_bool_flag and self.secondary_opts: + raise TypeError("Got secondary option for non boolean flag.") + if self.is_bool_flag and self.hide_input and self.prompt is not None: + raise TypeError("Hidden input does not work with boolean flag prompts.") + if self.count: + if self.multiple: + raise TypeError( + "Options cannot be multiple and count at the same time." + ) + elif self.is_flag: + raise TypeError( + "Options cannot be count and flags at the same time." + ) + + def _parse_decls(self, decls, expose_value): + opts = [] + secondary_opts = [] + name = None + possible_names = [] + + for decl in decls: + if isidentifier(decl): + if name is not None: + raise TypeError("Name defined twice") + name = decl + else: + split_char = ";" if decl[:1] == "/" else "/" + if split_char in decl: + first, second = decl.split(split_char, 1) + first = first.rstrip() + if first: + possible_names.append(split_opt(first)) + opts.append(first) + second = second.lstrip() + if second: + secondary_opts.append(second.lstrip()) + else: + possible_names.append(split_opt(decl)) + opts.append(decl) + + if name is None and possible_names: + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace("-", "_").lower() + if not isidentifier(name): + name = None + + if name is None: + if not expose_value: + return None, opts, secondary_opts + raise TypeError("Could not determine name for option") + + if not opts and not secondary_opts: + raise TypeError( + "No options defined but a name was passed ({}). Did you" + " mean to declare an argument instead of an option?".format(name) + ) + + return name, opts, secondary_opts + + def add_to_parser(self, parser, ctx): + kwargs = { + "dest": self.name, + "nargs": self.nargs, + "obj": self, + } + + if self.multiple: + action = "append" + elif self.count: + action = "count" + else: + action = "store" + + if self.is_flag: + kwargs.pop("nargs", None) + action_const = "{}_const".format(action) + if self.is_bool_flag and self.secondary_opts: + parser.add_option(self.opts, action=action_const, const=True, **kwargs) + parser.add_option( + self.secondary_opts, action=action_const, const=False, **kwargs + ) + else: + parser.add_option( + self.opts, action=action_const, const=self.flag_value, **kwargs + ) + else: + kwargs["action"] = action + parser.add_option(self.opts, **kwargs) + + def get_help_record(self, ctx): + if self.hidden: + return + any_prefix_is_slash = [] + + def _write_opts(opts): + rv, any_slashes = join_options(opts) + if any_slashes: + any_prefix_is_slash[:] = [True] + if not self.is_flag and not self.count: + rv += " {}".format(self.make_metavar()) + return rv + + rv = [_write_opts(self.opts)] + if self.secondary_opts: + rv.append(_write_opts(self.secondary_opts)) + + help = self.help or "" + extra = [] + if self.show_envvar: + envvar = self.envvar + if envvar is None: + if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None: + envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper()) + if envvar is not None: + extra.append( + "env var: {}".format( + ", ".join(str(d) for d in envvar) + if isinstance(envvar, (list, tuple)) + else envvar + ) + ) + if self.default is not None and (self.show_default or ctx.show_default): + if isinstance(self.show_default, string_types): + default_string = "({})".format(self.show_default) + elif isinstance(self.default, (list, tuple)): + default_string = ", ".join(str(d) for d in self.default) + elif inspect.isfunction(self.default): + default_string = "(dynamic)" + else: + default_string = self.default + extra.append("default: {}".format(default_string)) + + if self.required: + extra.append("required") + if extra: + help = "{}[{}]".format( + "{} ".format(help) if help else "", "; ".join(extra) + ) + + return ("; " if any_prefix_is_slash else " / ").join(rv), help + + def get_default(self, ctx): + # If we're a non boolean flag our default is more complex because + # we need to look at all flags in the same group to figure out + # if we're the the default one in which case we return the flag + # value as default. + if self.is_flag and not self.is_bool_flag: + for param in ctx.command.params: + if param.name == self.name and param.default: + return param.flag_value + return None + return Parameter.get_default(self, ctx) + + def prompt_for_value(self, ctx): + """This is an alternative flow that can be activated in the full + value processing if a value does not exist. It will prompt the + user until a valid value exists and then returns the processed + value as result. + """ + # Calculate the default before prompting anything to be stable. + default = self.get_default(ctx) + + # If this is a prompt for a flag we need to handle this + # differently. + if self.is_bool_flag: + return confirm(self.prompt, default) + + return prompt( + self.prompt, + default=default, + type=self.type, + hide_input=self.hide_input, + show_choices=self.show_choices, + confirmation_prompt=self.confirmation_prompt, + value_proc=lambda x: self.process_value(ctx, x), + ) + + def resolve_envvar_value(self, ctx): + rv = Parameter.resolve_envvar_value(self, ctx) + if rv is not None: + return rv + if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None: + envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper()) + return os.environ.get(envvar) + + def value_from_envvar(self, ctx): + rv = self.resolve_envvar_value(ctx) + if rv is None: + return None + value_depth = (self.nargs != 1) + bool(self.multiple) + if value_depth > 0 and rv is not None: + rv = self.type.split_envvar_value(rv) + if self.multiple and self.nargs != 1: + rv = batch(rv, self.nargs) + return rv + + def full_process_value(self, ctx, value): + if value is None and self.prompt is not None and not ctx.resilient_parsing: + return self.prompt_for_value(ctx) + return Parameter.full_process_value(self, ctx, value) + + +class Argument(Parameter): + """Arguments are positional parameters to a command. They generally + provide fewer features than options but can have infinite ``nargs`` + and are required by default. + + All parameters are passed onwards to the parameter constructor. + """ + + param_type_name = "argument" + + def __init__(self, param_decls, required=None, **attrs): + if required is None: + if attrs.get("default") is not None: + required = False + else: + required = attrs.get("nargs", 1) > 0 + Parameter.__init__(self, param_decls, required=required, **attrs) + if self.default is not None and self.nargs < 0: + raise TypeError( + "nargs=-1 in combination with a default value is not supported." + ) + + @property + def human_readable_name(self): + if self.metavar is not None: + return self.metavar + return self.name.upper() + + def make_metavar(self): + if self.metavar is not None: + return self.metavar + var = self.type.get_metavar(self) + if not var: + var = self.name.upper() + if not self.required: + var = "[{}]".format(var) + if self.nargs != 1: + var += "..." + return var + + def _parse_decls(self, decls, expose_value): + if not decls: + if not expose_value: + return None, [], [] + raise TypeError("Could not determine name for argument") + if len(decls) == 1: + name = arg = decls[0] + name = name.replace("-", "_").lower() + else: + raise TypeError( + "Arguments take exactly one parameter declaration, got" + " {}".format(len(decls)) + ) + return name, [arg], [] + + def get_usage_pieces(self, ctx): + return [self.make_metavar()] + + def get_error_hint(self, ctx): + return repr(self.make_metavar()) + + def add_to_parser(self, parser, ctx): + parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) diff --git a/openpype/vendor/python/python_2/click/decorators.py b/openpype/vendor/python/python_2/click/decorators.py new file mode 100644 index 0000000000..c7b5af6cc5 --- /dev/null +++ b/openpype/vendor/python/python_2/click/decorators.py @@ -0,0 +1,333 @@ +import inspect +import sys +from functools import update_wrapper + +from ._compat import iteritems +from ._unicodefun import _check_for_unicode_literals +from .core import Argument +from .core import Command +from .core import Group +from .core import Option +from .globals import get_current_context +from .utils import echo + + +def pass_context(f): + """Marks a callback as wanting to receive the current context + object as first argument. + """ + + def new_func(*args, **kwargs): + return f(get_current_context(), *args, **kwargs) + + return update_wrapper(new_func, f) + + +def pass_obj(f): + """Similar to :func:`pass_context`, but only pass the object on the + context onwards (:attr:`Context.obj`). This is useful if that object + represents the state of a nested system. + """ + + def new_func(*args, **kwargs): + return f(get_current_context().obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + +def make_pass_decorator(object_type, ensure=False): + """Given an object type this creates a decorator that will work + similar to :func:`pass_obj` but instead of passing the object of the + current context, it will find the innermost context of type + :func:`object_type`. + + This generates a decorator that works roughly like this:: + + from functools import update_wrapper + + def decorator(f): + @pass_context + def new_func(ctx, *args, **kwargs): + obj = ctx.find_object(object_type) + return ctx.invoke(f, obj, *args, **kwargs) + return update_wrapper(new_func, f) + return decorator + + :param object_type: the type of the object to pass. + :param ensure: if set to `True`, a new object will be created and + remembered on the context if it's not there yet. + """ + + def decorator(f): + def new_func(*args, **kwargs): + ctx = get_current_context() + if ensure: + obj = ctx.ensure_object(object_type) + else: + obj = ctx.find_object(object_type) + if obj is None: + raise RuntimeError( + "Managed to invoke callback without a context" + " object of type '{}' existing".format(object_type.__name__) + ) + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + return decorator + + +def _make_command(f, name, attrs, cls): + if isinstance(f, Command): + raise TypeError("Attempted to convert a callback into a command twice.") + try: + params = f.__click_params__ + params.reverse() + del f.__click_params__ + except AttributeError: + params = [] + help = attrs.get("help") + if help is None: + help = inspect.getdoc(f) + if isinstance(help, bytes): + help = help.decode("utf-8") + else: + help = inspect.cleandoc(help) + attrs["help"] = help + _check_for_unicode_literals() + return cls( + name=name or f.__name__.lower().replace("_", "-"), + callback=f, + params=params, + **attrs + ) + + +def command(name=None, cls=None, **attrs): + r"""Creates a new :class:`Command` and uses the decorated function as + callback. This will also automatically attach all decorated + :func:`option`\s and :func:`argument`\s as parameters to the command. + + The name of the command defaults to the name of the function with + underscores replaced by dashes. If you want to change that, you can + pass the intended name as the first argument. + + All keyword arguments are forwarded to the underlying command class. + + Once decorated the function turns into a :class:`Command` instance + that can be invoked as a command line utility or be attached to a + command :class:`Group`. + + :param name: the name of the command. This defaults to the function + name with underscores replaced by dashes. + :param cls: the command class to instantiate. This defaults to + :class:`Command`. + """ + if cls is None: + cls = Command + + def decorator(f): + cmd = _make_command(f, name, attrs, cls) + cmd.__doc__ = f.__doc__ + return cmd + + return decorator + + +def group(name=None, **attrs): + """Creates a new :class:`Group` with a function as callback. This + works otherwise the same as :func:`command` just that the `cls` + parameter is set to :class:`Group`. + """ + attrs.setdefault("cls", Group) + return command(name, **attrs) + + +def _param_memo(f, param): + if isinstance(f, Command): + f.params.append(param) + else: + if not hasattr(f, "__click_params__"): + f.__click_params__ = [] + f.__click_params__.append(param) + + +def argument(*param_decls, **attrs): + """Attaches an argument to the command. All positional arguments are + passed as parameter declarations to :class:`Argument`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Argument` instance manually + and attaching it to the :attr:`Command.params` list. + + :param cls: the argument class to instantiate. This defaults to + :class:`Argument`. + """ + + def decorator(f): + ArgumentClass = attrs.pop("cls", Argument) + _param_memo(f, ArgumentClass(param_decls, **attrs)) + return f + + return decorator + + +def option(*param_decls, **attrs): + """Attaches an option to the command. All positional arguments are + passed as parameter declarations to :class:`Option`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Option` instance manually + and attaching it to the :attr:`Command.params` list. + + :param cls: the option class to instantiate. This defaults to + :class:`Option`. + """ + + def decorator(f): + # Issue 926, copy attrs, so pre-defined options can re-use the same cls= + option_attrs = attrs.copy() + + if "help" in option_attrs: + option_attrs["help"] = inspect.cleandoc(option_attrs["help"]) + OptionClass = option_attrs.pop("cls", Option) + _param_memo(f, OptionClass(param_decls, **option_attrs)) + return f + + return decorator + + +def confirmation_option(*param_decls, **attrs): + """Shortcut for confirmation prompts that can be ignored by passing + ``--yes`` as parameter. + + This is equivalent to decorating a function with :func:`option` with + the following parameters:: + + def callback(ctx, param, value): + if not value: + ctx.abort() + + @click.command() + @click.option('--yes', is_flag=True, callback=callback, + expose_value=False, prompt='Do you want to continue?') + def dropdb(): + pass + """ + + def decorator(f): + def callback(ctx, param, value): + if not value: + ctx.abort() + + attrs.setdefault("is_flag", True) + attrs.setdefault("callback", callback) + attrs.setdefault("expose_value", False) + attrs.setdefault("prompt", "Do you want to continue?") + attrs.setdefault("help", "Confirm the action without prompting.") + return option(*(param_decls or ("--yes",)), **attrs)(f) + + return decorator + + +def password_option(*param_decls, **attrs): + """Shortcut for password prompts. + + This is equivalent to decorating a function with :func:`option` with + the following parameters:: + + @click.command() + @click.option('--password', prompt=True, confirmation_prompt=True, + hide_input=True) + def changeadmin(password): + pass + """ + + def decorator(f): + attrs.setdefault("prompt", True) + attrs.setdefault("confirmation_prompt", True) + attrs.setdefault("hide_input", True) + return option(*(param_decls or ("--password",)), **attrs)(f) + + return decorator + + +def version_option(version=None, *param_decls, **attrs): + """Adds a ``--version`` option which immediately ends the program + printing out the version number. This is implemented as an eager + option that prints the version and exits the program in the callback. + + :param version: the version number to show. If not provided Click + attempts an auto discovery via setuptools. + :param prog_name: the name of the program (defaults to autodetection) + :param message: custom message to show instead of the default + (``'%(prog)s, version %(version)s'``) + :param others: everything else is forwarded to :func:`option`. + """ + if version is None: + if hasattr(sys, "_getframe"): + module = sys._getframe(1).f_globals.get("__name__") + else: + module = "" + + def decorator(f): + prog_name = attrs.pop("prog_name", None) + message = attrs.pop("message", "%(prog)s, version %(version)s") + + def callback(ctx, param, value): + if not value or ctx.resilient_parsing: + return + prog = prog_name + if prog is None: + prog = ctx.find_root().info_name + ver = version + if ver is None: + try: + import pkg_resources + except ImportError: + pass + else: + for dist in pkg_resources.working_set: + scripts = dist.get_entry_map().get("console_scripts") or {} + for _, entry_point in iteritems(scripts): + if entry_point.module_name == module: + ver = dist.version + break + if ver is None: + raise RuntimeError("Could not determine version") + echo(message % {"prog": prog, "version": ver}, color=ctx.color) + ctx.exit() + + attrs.setdefault("is_flag", True) + attrs.setdefault("expose_value", False) + attrs.setdefault("is_eager", True) + attrs.setdefault("help", "Show the version and exit.") + attrs["callback"] = callback + return option(*(param_decls or ("--version",)), **attrs)(f) + + return decorator + + +def help_option(*param_decls, **attrs): + """Adds a ``--help`` option which immediately ends the program + printing out the help page. This is usually unnecessary to add as + this is added by default to all commands unless suppressed. + + Like :func:`version_option`, this is implemented as eager option that + prints in the callback and exits. + + All arguments are forwarded to :func:`option`. + """ + + def decorator(f): + def callback(ctx, param, value): + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + attrs.setdefault("is_flag", True) + attrs.setdefault("expose_value", False) + attrs.setdefault("help", "Show this message and exit.") + attrs.setdefault("is_eager", True) + attrs["callback"] = callback + return option(*(param_decls or ("--help",)), **attrs)(f) + + return decorator diff --git a/openpype/vendor/python/python_2/click/exceptions.py b/openpype/vendor/python/python_2/click/exceptions.py new file mode 100644 index 0000000000..592ee38f0d --- /dev/null +++ b/openpype/vendor/python/python_2/click/exceptions.py @@ -0,0 +1,253 @@ +from ._compat import filename_to_ui +from ._compat import get_text_stderr +from ._compat import PY2 +from .utils import echo + + +def _join_param_hints(param_hint): + if isinstance(param_hint, (tuple, list)): + return " / ".join(repr(x) for x in param_hint) + return param_hint + + +class ClickException(Exception): + """An exception that Click can handle and show to the user.""" + + #: The exit code for this exception + exit_code = 1 + + def __init__(self, message): + ctor_msg = message + if PY2: + if ctor_msg is not None: + ctor_msg = ctor_msg.encode("utf-8") + Exception.__init__(self, ctor_msg) + self.message = message + + def format_message(self): + return self.message + + def __str__(self): + return self.message + + if PY2: + __unicode__ = __str__ + + def __str__(self): + return self.message.encode("utf-8") + + def show(self, file=None): + if file is None: + file = get_text_stderr() + echo("Error: {}".format(self.format_message()), file=file) + + +class UsageError(ClickException): + """An internal exception that signals a usage error. This typically + aborts any further handling. + + :param message: the error message to display. + :param ctx: optionally the context that caused this error. Click will + fill in the context automatically in some situations. + """ + + exit_code = 2 + + def __init__(self, message, ctx=None): + ClickException.__init__(self, message) + self.ctx = ctx + self.cmd = self.ctx.command if self.ctx else None + + def show(self, file=None): + if file is None: + file = get_text_stderr() + color = None + hint = "" + if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None: + hint = "Try '{} {}' for help.\n".format( + self.ctx.command_path, self.ctx.help_option_names[0] + ) + if self.ctx is not None: + color = self.ctx.color + echo("{}\n{}".format(self.ctx.get_usage(), hint), file=file, color=color) + echo("Error: {}".format(self.format_message()), file=file, color=color) + + +class BadParameter(UsageError): + """An exception that formats out a standardized error message for a + bad parameter. This is useful when thrown from a callback or type as + Click will attach contextual information to it (for instance, which + parameter it is). + + .. versionadded:: 2.0 + + :param param: the parameter object that caused this error. This can + be left out, and Click will attach this info itself + if possible. + :param param_hint: a string that shows up as parameter name. This + can be used as alternative to `param` in cases + where custom validation should happen. If it is + a string it's used as such, if it's a list then + each item is quoted and separated. + """ + + def __init__(self, message, ctx=None, param=None, param_hint=None): + UsageError.__init__(self, message, ctx) + self.param = param + self.param_hint = param_hint + + def format_message(self): + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) + else: + return "Invalid value: {}".format(self.message) + param_hint = _join_param_hints(param_hint) + + return "Invalid value for {}: {}".format(param_hint, self.message) + + +class MissingParameter(BadParameter): + """Raised if click required an option or argument but it was not + provided when invoking the script. + + .. versionadded:: 4.0 + + :param param_type: a string that indicates the type of the parameter. + The default is to inherit the parameter type from + the given `param`. Valid values are ``'parameter'``, + ``'option'`` or ``'argument'``. + """ + + def __init__( + self, message=None, ctx=None, param=None, param_hint=None, param_type=None + ): + BadParameter.__init__(self, message, ctx, param, param_hint) + self.param_type = param_type + + def format_message(self): + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) + else: + param_hint = None + param_hint = _join_param_hints(param_hint) + + param_type = self.param_type + if param_type is None and self.param is not None: + param_type = self.param.param_type_name + + msg = self.message + if self.param is not None: + msg_extra = self.param.type.get_missing_message(self.param) + if msg_extra: + if msg: + msg += ". {}".format(msg_extra) + else: + msg = msg_extra + + return "Missing {}{}{}{}".format( + param_type, + " {}".format(param_hint) if param_hint else "", + ". " if msg else ".", + msg or "", + ) + + def __str__(self): + if self.message is None: + param_name = self.param.name if self.param else None + return "missing parameter: {}".format(param_name) + else: + return self.message + + if PY2: + __unicode__ = __str__ + + def __str__(self): + return self.__unicode__().encode("utf-8") + + +class NoSuchOption(UsageError): + """Raised if click attempted to handle an option that does not + exist. + + .. versionadded:: 4.0 + """ + + def __init__(self, option_name, message=None, possibilities=None, ctx=None): + if message is None: + message = "no such option: {}".format(option_name) + UsageError.__init__(self, message, ctx) + self.option_name = option_name + self.possibilities = possibilities + + def format_message(self): + bits = [self.message] + if self.possibilities: + if len(self.possibilities) == 1: + bits.append("Did you mean {}?".format(self.possibilities[0])) + else: + possibilities = sorted(self.possibilities) + bits.append("(Possible options: {})".format(", ".join(possibilities))) + return " ".join(bits) + + +class BadOptionUsage(UsageError): + """Raised if an option is generally supplied but the use of the option + was incorrect. This is for instance raised if the number of arguments + for an option is not correct. + + .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. + """ + + def __init__(self, option_name, message, ctx=None): + UsageError.__init__(self, message, ctx) + self.option_name = option_name + + +class BadArgumentUsage(UsageError): + """Raised if an argument is generally supplied but the use of the argument + was incorrect. This is for instance raised if the number of values + for an argument is not correct. + + .. versionadded:: 6.0 + """ + + def __init__(self, message, ctx=None): + UsageError.__init__(self, message, ctx) + + +class FileError(ClickException): + """Raised if a file cannot be opened.""" + + def __init__(self, filename, hint=None): + ui_filename = filename_to_ui(filename) + if hint is None: + hint = "unknown error" + ClickException.__init__(self, hint) + self.ui_filename = ui_filename + self.filename = filename + + def format_message(self): + return "Could not open file {}: {}".format(self.ui_filename, self.message) + + +class Abort(RuntimeError): + """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + + __slots__ = ("exit_code",) + + def __init__(self, code=0): + self.exit_code = code diff --git a/openpype/vendor/python/python_2/click/formatting.py b/openpype/vendor/python/python_2/click/formatting.py new file mode 100644 index 0000000000..319c7f6163 --- /dev/null +++ b/openpype/vendor/python/python_2/click/formatting.py @@ -0,0 +1,283 @@ +from contextlib import contextmanager + +from ._compat import term_len +from .parser import split_opt +from .termui import get_terminal_size + +# Can force a width. This is used by the test system +FORCED_WIDTH = None + + +def measure_table(rows): + widths = {} + for row in rows: + for idx, col in enumerate(row): + widths[idx] = max(widths.get(idx, 0), term_len(col)) + return tuple(y for x, y in sorted(widths.items())) + + +def iter_rows(rows, col_count): + for row in rows: + row = tuple(row) + yield row + ("",) * (col_count - len(row)) + + +def wrap_text( + text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False +): + """A helper function that intelligently wraps text. By default, it + assumes that it operates on a single paragraph of text but if the + `preserve_paragraphs` parameter is provided it will intelligently + handle paragraphs (defined by two empty lines). + + If paragraphs are handled, a paragraph can be prefixed with an empty + line containing the ``\\b`` character (``\\x08``) to indicate that + no rewrapping should happen in that block. + + :param text: the text that should be rewrapped. + :param width: the maximum width for the text. + :param initial_indent: the initial indent that should be placed on the + first line as a string. + :param subsequent_indent: the indent string that should be placed on + each consecutive line. + :param preserve_paragraphs: if this flag is set then the wrapping will + intelligently handle paragraphs. + """ + from ._textwrap import TextWrapper + + text = text.expandtabs() + wrapper = TextWrapper( + width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + replace_whitespace=False, + ) + if not preserve_paragraphs: + return wrapper.fill(text) + + p = [] + buf = [] + indent = None + + def _flush_par(): + if not buf: + return + if buf[0].strip() == "\b": + p.append((indent or 0, True, "\n".join(buf[1:]))) + else: + p.append((indent or 0, False, " ".join(buf))) + del buf[:] + + for line in text.splitlines(): + if not line: + _flush_par() + indent = None + else: + if indent is None: + orig_len = term_len(line) + line = line.lstrip() + indent = orig_len - term_len(line) + buf.append(line) + _flush_par() + + rv = [] + for indent, raw, text in p: + with wrapper.extra_indent(" " * indent): + if raw: + rv.append(wrapper.indent_only(text)) + else: + rv.append(wrapper.fill(text)) + + return "\n\n".join(rv) + + +class HelpFormatter(object): + """This class helps with formatting text-based help pages. It's + usually just needed for very special internal cases, but it's also + exposed so that developers can write their own fancy outputs. + + At present, it always writes into memory. + + :param indent_increment: the additional increment for each level. + :param width: the width for the text. This defaults to the terminal + width clamped to a maximum of 78. + """ + + def __init__(self, indent_increment=2, width=None, max_width=None): + self.indent_increment = indent_increment + if max_width is None: + max_width = 80 + if width is None: + width = FORCED_WIDTH + if width is None: + width = max(min(get_terminal_size()[0], max_width) - 2, 50) + self.width = width + self.current_indent = 0 + self.buffer = [] + + def write(self, string): + """Writes a unicode string into the internal buffer.""" + self.buffer.append(string) + + def indent(self): + """Increases the indentation.""" + self.current_indent += self.indent_increment + + def dedent(self): + """Decreases the indentation.""" + self.current_indent -= self.indent_increment + + def write_usage(self, prog, args="", prefix="Usage: "): + """Writes a usage line into the buffer. + + :param prog: the program name. + :param args: whitespace separated list of arguments. + :param prefix: the prefix for the first line. + """ + usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent) + text_width = self.width - self.current_indent + + if text_width >= (term_len(usage_prefix) + 20): + # The arguments will fit to the right of the prefix. + indent = " " * term_len(usage_prefix) + self.write( + wrap_text( + args, + text_width, + initial_indent=usage_prefix, + subsequent_indent=indent, + ) + ) + else: + # The prefix is too long, put the arguments on the next line. + self.write(usage_prefix) + self.write("\n") + indent = " " * (max(self.current_indent, term_len(prefix)) + 4) + self.write( + wrap_text( + args, text_width, initial_indent=indent, subsequent_indent=indent + ) + ) + + self.write("\n") + + def write_heading(self, heading): + """Writes a heading into the buffer.""" + self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent)) + + def write_paragraph(self): + """Writes a paragraph into the buffer.""" + if self.buffer: + self.write("\n") + + def write_text(self, text): + """Writes re-indented text into the buffer. This rewraps and + preserves paragraphs. + """ + text_width = max(self.width - self.current_indent, 11) + indent = " " * self.current_indent + self.write( + wrap_text( + text, + text_width, + initial_indent=indent, + subsequent_indent=indent, + preserve_paragraphs=True, + ) + ) + self.write("\n") + + def write_dl(self, rows, col_max=30, col_spacing=2): + """Writes a definition list into the buffer. This is how options + and commands are usually formatted. + + :param rows: a list of two item tuples for the terms and values. + :param col_max: the maximum width of the first column. + :param col_spacing: the number of spaces between the first and + second column. + """ + rows = list(rows) + widths = measure_table(rows) + if len(widths) != 2: + raise TypeError("Expected two columns for definition list") + + first_col = min(widths[0], col_max) + col_spacing + + for first, second in iter_rows(rows, len(widths)): + self.write("{:>{w}}{}".format("", first, w=self.current_indent)) + if not second: + self.write("\n") + continue + if term_len(first) <= first_col - col_spacing: + self.write(" " * (first_col - term_len(first))) + else: + self.write("\n") + self.write(" " * (first_col + self.current_indent)) + + text_width = max(self.width - first_col - 2, 10) + wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) + lines = wrapped_text.splitlines() + + if lines: + self.write("{}\n".format(lines[0])) + + for line in lines[1:]: + self.write( + "{:>{w}}{}\n".format( + "", line, w=first_col + self.current_indent + ) + ) + + if len(lines) > 1: + # separate long help from next option + self.write("\n") + else: + self.write("\n") + + @contextmanager + def section(self, name): + """Helpful context manager that writes a paragraph, a heading, + and the indents. + + :param name: the section name that is written as heading. + """ + self.write_paragraph() + self.write_heading(name) + self.indent() + try: + yield + finally: + self.dedent() + + @contextmanager + def indentation(self): + """A context manager that increases the indentation.""" + self.indent() + try: + yield + finally: + self.dedent() + + def getvalue(self): + """Returns the buffer contents.""" + return "".join(self.buffer) + + +def join_options(options): + """Given a list of option strings this joins them in the most appropriate + way and returns them in the form ``(formatted_string, + any_prefix_is_slash)`` where the second item in the tuple is a flag that + indicates if any of the option prefixes was a slash. + """ + rv = [] + any_prefix_is_slash = False + for opt in options: + prefix = split_opt(opt)[0] + if prefix == "/": + any_prefix_is_slash = True + rv.append((len(prefix), opt)) + + rv.sort(key=lambda x: x[0]) + + rv = ", ".join(x[1] for x in rv) + return rv, any_prefix_is_slash diff --git a/openpype/vendor/python/python_2/click/globals.py b/openpype/vendor/python/python_2/click/globals.py new file mode 100644 index 0000000000..1649f9a0bf --- /dev/null +++ b/openpype/vendor/python/python_2/click/globals.py @@ -0,0 +1,47 @@ +from threading import local + +_local = local() + + +def get_current_context(silent=False): + """Returns the current click context. This can be used as a way to + access the current context object from anywhere. This is a more implicit + alternative to the :func:`pass_context` decorator. This function is + primarily useful for helpers such as :func:`echo` which might be + interested in changing its behavior based on the current context. + + To push the current context, :meth:`Context.scope` can be used. + + .. versionadded:: 5.0 + + :param silent: if set to `True` the return value is `None` if no context + is available. The default behavior is to raise a + :exc:`RuntimeError`. + """ + try: + return _local.stack[-1] + except (AttributeError, IndexError): + if not silent: + raise RuntimeError("There is no active click context.") + + +def push_context(ctx): + """Pushes a new context to the current stack.""" + _local.__dict__.setdefault("stack", []).append(ctx) + + +def pop_context(): + """Removes the top level from the stack.""" + _local.stack.pop() + + +def resolve_color_default(color=None): + """"Internal helper to get the default value of the color flag. If a + value is passed it's returned unchanged, otherwise it's looked up from + the current context. + """ + if color is not None: + return color + ctx = get_current_context(silent=True) + if ctx is not None: + return ctx.color diff --git a/openpype/vendor/python/python_2/click/parser.py b/openpype/vendor/python/python_2/click/parser.py new file mode 100644 index 0000000000..f43ebfe9fc --- /dev/null +++ b/openpype/vendor/python/python_2/click/parser.py @@ -0,0 +1,428 @@ +# -*- coding: utf-8 -*- +""" +This module started out as largely a copy paste from the stdlib's +optparse module with the features removed that we do not need from +optparse because we implement them in Click on a higher level (for +instance type handling, help formatting and a lot more). + +The plan is to remove more and more from here over time. + +The reason this is a different module and not optparse from the stdlib +is that there are differences in 2.x and 3.x about the error messages +generated and optparse in the stdlib uses gettext for no good reason +and might cause us issues. + +Click uses parts of optparse written by Gregory P. Ward and maintained +by the Python Software Foundation. This is limited to code in parser.py. + +Copyright 2001-2006 Gregory P. Ward. All rights reserved. +Copyright 2002-2006 Python Software Foundation. All rights reserved. +""" +import re +from collections import deque + +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import NoSuchOption +from .exceptions import UsageError + + +def _unpack_args(args, nargs_spec): + """Given an iterable of arguments and an iterable of nargs specifications, + it returns a tuple with all the unpacked arguments at the first index + and all remaining arguments as the second. + + The nargs specification is the number of arguments that should be consumed + or `-1` to indicate that this position should eat up all the remainders. + + Missing items are filled with `None`. + """ + args = deque(args) + nargs_spec = deque(nargs_spec) + rv = [] + spos = None + + def _fetch(c): + try: + if spos is None: + return c.popleft() + else: + return c.pop() + except IndexError: + return None + + while nargs_spec: + nargs = _fetch(nargs_spec) + if nargs == 1: + rv.append(_fetch(args)) + elif nargs > 1: + x = [_fetch(args) for _ in range(nargs)] + # If we're reversed, we're pulling in the arguments in reverse, + # so we need to turn them around. + if spos is not None: + x.reverse() + rv.append(tuple(x)) + elif nargs < 0: + if spos is not None: + raise TypeError("Cannot have two nargs < 0") + spos = len(rv) + rv.append(None) + + # spos is the position of the wildcard (star). If it's not `None`, + # we fill it with the remainder. + if spos is not None: + rv[spos] = tuple(args) + args = [] + rv[spos + 1 :] = reversed(rv[spos + 1 :]) + + return tuple(rv), list(args) + + +def _error_opt_args(nargs, opt): + if nargs == 1: + raise BadOptionUsage(opt, "{} option requires an argument".format(opt)) + raise BadOptionUsage(opt, "{} option requires {} arguments".format(opt, nargs)) + + +def split_opt(opt): + first = opt[:1] + if first.isalnum(): + return "", opt + if opt[1:2] == first: + return opt[:2], opt[2:] + return first, opt[1:] + + +def normalize_opt(opt, ctx): + if ctx is None or ctx.token_normalize_func is None: + return opt + prefix, opt = split_opt(opt) + return prefix + ctx.token_normalize_func(opt) + + +def split_arg_string(string): + """Given an argument string this attempts to split it into small parts.""" + rv = [] + for match in re.finditer( + r"('([^'\\]*(?:\\.[^'\\]*)*)'|\"([^\"\\]*(?:\\.[^\"\\]*)*)\"|\S+)\s*", + string, + re.S, + ): + arg = match.group().strip() + if arg[:1] == arg[-1:] and arg[:1] in "\"'": + arg = arg[1:-1].encode("ascii", "backslashreplace").decode("unicode-escape") + try: + arg = type(string)(arg) + except UnicodeError: + pass + rv.append(arg) + return rv + + +class Option(object): + def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None): + self._short_opts = [] + self._long_opts = [] + self.prefixes = set() + + for opt in opts: + prefix, value = split_opt(opt) + if not prefix: + raise ValueError("Invalid start character for option ({})".format(opt)) + self.prefixes.add(prefix[0]) + if len(prefix) == 1 and len(value) == 1: + self._short_opts.append(opt) + else: + self._long_opts.append(opt) + self.prefixes.add(prefix) + + if action is None: + action = "store" + + self.dest = dest + self.action = action + self.nargs = nargs + self.const = const + self.obj = obj + + @property + def takes_value(self): + return self.action in ("store", "append") + + def process(self, value, state): + if self.action == "store": + state.opts[self.dest] = value + elif self.action == "store_const": + state.opts[self.dest] = self.const + elif self.action == "append": + state.opts.setdefault(self.dest, []).append(value) + elif self.action == "append_const": + state.opts.setdefault(self.dest, []).append(self.const) + elif self.action == "count": + state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 + else: + raise ValueError("unknown action '{}'".format(self.action)) + state.order.append(self.obj) + + +class Argument(object): + def __init__(self, dest, nargs=1, obj=None): + self.dest = dest + self.nargs = nargs + self.obj = obj + + def process(self, value, state): + if self.nargs > 1: + holes = sum(1 for x in value if x is None) + if holes == len(value): + value = None + elif holes != 0: + raise BadArgumentUsage( + "argument {} takes {} values".format(self.dest, self.nargs) + ) + state.opts[self.dest] = value + state.order.append(self.obj) + + +class ParsingState(object): + def __init__(self, rargs): + self.opts = {} + self.largs = [] + self.rargs = rargs + self.order = [] + + +class OptionParser(object): + """The option parser is an internal class that is ultimately used to + parse options and arguments. It's modelled after optparse and brings + a similar but vastly simplified API. It should generally not be used + directly as the high level Click classes wrap it for you. + + It's not nearly as extensible as optparse or argparse as it does not + implement features that are implemented on a higher level (such as + types or defaults). + + :param ctx: optionally the :class:`~click.Context` where this parser + should go with. + """ + + def __init__(self, ctx=None): + #: The :class:`~click.Context` for this parser. This might be + #: `None` for some advanced use cases. + self.ctx = ctx + #: This controls how the parser deals with interspersed arguments. + #: If this is set to `False`, the parser will stop on the first + #: non-option. Click uses this to implement nested subcommands + #: safely. + self.allow_interspersed_args = True + #: This tells the parser how to deal with unknown options. By + #: default it will error out (which is sensible), but there is a + #: second mode where it will ignore it and continue processing + #: after shifting all the unknown options into the resulting args. + self.ignore_unknown_options = False + if ctx is not None: + self.allow_interspersed_args = ctx.allow_interspersed_args + self.ignore_unknown_options = ctx.ignore_unknown_options + self._short_opt = {} + self._long_opt = {} + self._opt_prefixes = {"-", "--"} + self._args = [] + + def add_option(self, opts, dest, action=None, nargs=1, const=None, obj=None): + """Adds a new option named `dest` to the parser. The destination + is not inferred (unlike with optparse) and needs to be explicitly + provided. Action can be any of ``store``, ``store_const``, + ``append``, ``appnd_const`` or ``count``. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + if obj is None: + obj = dest + opts = [normalize_opt(opt, self.ctx) for opt in opts] + option = Option(opts, dest, action=action, nargs=nargs, const=const, obj=obj) + self._opt_prefixes.update(option.prefixes) + for opt in option._short_opts: + self._short_opt[opt] = option + for opt in option._long_opts: + self._long_opt[opt] = option + + def add_argument(self, dest, nargs=1, obj=None): + """Adds a positional argument named `dest` to the parser. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + if obj is None: + obj = dest + self._args.append(Argument(dest=dest, nargs=nargs, obj=obj)) + + def parse_args(self, args): + """Parses positional arguments and returns ``(values, args, order)`` + for the parsed options and arguments as well as the leftover + arguments if there are any. The order is a list of objects as they + appear on the command line. If arguments appear multiple times they + will be memorized multiple times as well. + """ + state = ParsingState(args) + try: + self._process_args_for_options(state) + self._process_args_for_args(state) + except UsageError: + if self.ctx is None or not self.ctx.resilient_parsing: + raise + return state.opts, state.largs, state.order + + def _process_args_for_args(self, state): + pargs, args = _unpack_args( + state.largs + state.rargs, [x.nargs for x in self._args] + ) + + for idx, arg in enumerate(self._args): + arg.process(pargs[idx], state) + + state.largs = args + state.rargs = [] + + def _process_args_for_options(self, state): + while state.rargs: + arg = state.rargs.pop(0) + arglen = len(arg) + # Double dashes always handled explicitly regardless of what + # prefixes are valid. + if arg == "--": + return + elif arg[:1] in self._opt_prefixes and arglen > 1: + self._process_opts(arg, state) + elif self.allow_interspersed_args: + state.largs.append(arg) + else: + state.rargs.insert(0, arg) + return + + # Say this is the original argument list: + # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)] + # ^ + # (we are about to process arg(i)). + # + # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of + # [arg0, ..., arg(i-1)] (any options and their arguments will have + # been removed from largs). + # + # The while loop will usually consume 1 or more arguments per pass. + # If it consumes 1 (eg. arg is an option that takes no arguments), + # then after _process_arg() is done the situation is: + # + # largs = subset of [arg0, ..., arg(i)] + # rargs = [arg(i+1), ..., arg(N-1)] + # + # If allow_interspersed_args is false, largs will always be + # *empty* -- still a subset of [arg0, ..., arg(i-1)], but + # not a very interesting subset! + + def _match_long_opt(self, opt, explicit_value, state): + if opt not in self._long_opt: + possibilities = [word for word in self._long_opt if word.startswith(opt)] + raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) + + option = self._long_opt[opt] + if option.takes_value: + # At this point it's safe to modify rargs by injecting the + # explicit value, because no exception is raised in this + # branch. This means that the inserted value will be fully + # consumed. + if explicit_value is not None: + state.rargs.insert(0, explicit_value) + + nargs = option.nargs + if len(state.rargs) < nargs: + _error_opt_args(nargs, opt) + elif nargs == 1: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + elif explicit_value is not None: + raise BadOptionUsage(opt, "{} option does not take a value".format(opt)) + + else: + value = None + + option.process(value, state) + + def _match_short_opt(self, arg, state): + stop = False + i = 1 + prefix = arg[0] + unknown_options = [] + + for ch in arg[1:]: + opt = normalize_opt(prefix + ch, self.ctx) + option = self._short_opt.get(opt) + i += 1 + + if not option: + if self.ignore_unknown_options: + unknown_options.append(ch) + continue + raise NoSuchOption(opt, ctx=self.ctx) + if option.takes_value: + # Any characters left in arg? Pretend they're the + # next arg, and stop consuming characters of arg. + if i < len(arg): + state.rargs.insert(0, arg[i:]) + stop = True + + nargs = option.nargs + if len(state.rargs) < nargs: + _error_opt_args(nargs, opt) + elif nargs == 1: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + else: + value = None + + option.process(value, state) + + if stop: + break + + # If we got any unknown options we re-combinate the string of the + # remaining options and re-attach the prefix, then report that + # to the state as new larg. This way there is basic combinatorics + # that can be achieved while still ignoring unknown arguments. + if self.ignore_unknown_options and unknown_options: + state.largs.append("{}{}".format(prefix, "".join(unknown_options))) + + def _process_opts(self, arg, state): + explicit_value = None + # Long option handling happens in two parts. The first part is + # supporting explicitly attached values. In any case, we will try + # to long match the option first. + if "=" in arg: + long_opt, explicit_value = arg.split("=", 1) + else: + long_opt = arg + norm_long_opt = normalize_opt(long_opt, self.ctx) + + # At this point we will match the (assumed) long option through + # the long option matching code. Note that this allows options + # like "-foo" to be matched as long options. + try: + self._match_long_opt(norm_long_opt, explicit_value, state) + except NoSuchOption: + # At this point the long option matching failed, and we need + # to try with short options. However there is a special rule + # which says, that if we have a two character options prefix + # (applies to "--foo" for instance), we do not dispatch to the + # short option code and will instead raise the no option + # error. + if arg[:2] not in self._opt_prefixes: + return self._match_short_opt(arg, state) + if not self.ignore_unknown_options: + raise + state.largs.append(arg) diff --git a/openpype/vendor/python/python_2/click/termui.py b/openpype/vendor/python/python_2/click/termui.py new file mode 100644 index 0000000000..02ef9e9f04 --- /dev/null +++ b/openpype/vendor/python/python_2/click/termui.py @@ -0,0 +1,681 @@ +import inspect +import io +import itertools +import os +import struct +import sys + +from ._compat import DEFAULT_COLUMNS +from ._compat import get_winterm_size +from ._compat import isatty +from ._compat import raw_input +from ._compat import string_types +from ._compat import strip_ansi +from ._compat import text_type +from ._compat import WIN +from .exceptions import Abort +from .exceptions import UsageError +from .globals import resolve_color_default +from .types import Choice +from .types import convert_type +from .types import Path +from .utils import echo +from .utils import LazyFile + +# The prompt functions to use. The doc tools currently override these +# functions to customize how they work. +visible_prompt_func = raw_input + +_ansi_colors = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "reset": 39, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, +} +_ansi_reset_all = "\033[0m" + + +def hidden_prompt_func(prompt): + import getpass + + return getpass.getpass(prompt) + + +def _build_prompt( + text, suffix, show_default=False, default=None, show_choices=True, type=None +): + prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += " ({})".format(", ".join(map(str, type.choices))) + if default is not None and show_default: + prompt = "{} [{}]".format(prompt, _format_default(default)) + return prompt + suffix + + +def _format_default(default): + if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"): + return default.name + + return default + + +def prompt( + text, + default=None, + hide_input=False, + confirmation_prompt=False, + type=None, + value_proc=None, + prompt_suffix=": ", + show_default=True, + err=False, + show_choices=True, +): + """Prompts a user for input. This is a convenience function that can + be used to prompt a user for input later. + + If the user aborts the input by sending a interrupt signal, this + function will catch it and raise a :exc:`Abort` exception. + + .. versionadded:: 7.0 + Added the show_choices parameter. + + .. versionadded:: 6.0 + Added unicode support for cmd.exe on Windows. + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param text: the text to show for the prompt. + :param default: the default value to use if no input happens. If this + is not given it will prompt until it's aborted. + :param hide_input: if this is set to true then the input value will + be hidden. + :param confirmation_prompt: asks for confirmation for the value. + :param type: the type to use to check the value against. + :param value_proc: if this parameter is provided it's a function that + is invoked instead of the type conversion to + convert a value. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". + """ + result = None + + def prompt_func(text): + f = hidden_prompt_func if hide_input else visible_prompt_func + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(text, nl=False, err=err) + return f("") + except (KeyboardInterrupt, EOFError): + # getpass doesn't print a newline if the user aborts input with ^C. + # Allegedly this behavior is inherited from getpass(3). + # A doc bug has been filed at https://bugs.python.org/issue24711 + if hide_input: + echo(None, err=err) + raise Abort() + + if value_proc is None: + value_proc = convert_type(type, default) + + prompt = _build_prompt( + text, prompt_suffix, show_default, default, show_choices, type + ) + + while 1: + while 1: + value = prompt_func(prompt) + if value: + break + elif default is not None: + if isinstance(value_proc, Path): + # validate Path default value(exists, dir_okay etc.) + value = default + break + return default + try: + result = value_proc(value) + except UsageError as e: + echo("Error: {}".format(e.message), err=err) # noqa: B306 + continue + if not confirmation_prompt: + return result + while 1: + value2 = prompt_func("Repeat for confirmation: ") + if value2: + break + if value == value2: + return result + echo("Error: the two entered values do not match", err=err) + + +def confirm( + text, default=False, abort=False, prompt_suffix=": ", show_default=True, err=False +): + """Prompts for confirmation (yes/no question). + + If the user aborts the input by sending a interrupt signal this + function will catch it and raise a :exc:`Abort` exception. + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param text: the question to ask. + :param default: the default for the prompt. + :param abort: if this is set to `True` a negative answer aborts the + exception by raising :exc:`Abort`. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + prompt = _build_prompt( + text, prompt_suffix, show_default, "Y/n" if default else "y/N" + ) + while 1: + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(prompt, nl=False, err=err) + value = visible_prompt_func("").lower().strip() + except (KeyboardInterrupt, EOFError): + raise Abort() + if value in ("y", "yes"): + rv = True + elif value in ("n", "no"): + rv = False + elif value == "": + rv = default + else: + echo("Error: invalid input", err=err) + continue + break + if abort and not rv: + raise Abort() + return rv + + +def get_terminal_size(): + """Returns the current size of the terminal as tuple in the form + ``(width, height)`` in columns and rows. + """ + # If shutil has get_terminal_size() (Python 3.3 and later) use that + if sys.version_info >= (3, 3): + import shutil + + shutil_get_terminal_size = getattr(shutil, "get_terminal_size", None) + if shutil_get_terminal_size: + sz = shutil_get_terminal_size() + return sz.columns, sz.lines + + # We provide a sensible default for get_winterm_size() when being invoked + # inside a subprocess. Without this, it would not provide a useful input. + if get_winterm_size is not None: + size = get_winterm_size() + if size == (0, 0): + return (79, 24) + else: + return size + + def ioctl_gwinsz(fd): + try: + import fcntl + import termios + + cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) + except Exception: + return + return cr + + cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2) + if not cr: + try: + fd = os.open(os.ctermid(), os.O_RDONLY) + try: + cr = ioctl_gwinsz(fd) + finally: + os.close(fd) + except Exception: + pass + if not cr or not cr[0] or not cr[1]: + cr = (os.environ.get("LINES", 25), os.environ.get("COLUMNS", DEFAULT_COLUMNS)) + return int(cr[1]), int(cr[0]) + + +def echo_via_pager(text_or_generator, color=None): + """This function takes a text and shows it via an environment specific + pager on stdout. + + .. versionchanged:: 3.0 + Added the `color` flag. + + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + color = resolve_color_default(color) + + if inspect.isgeneratorfunction(text_or_generator): + i = text_or_generator() + elif isinstance(text_or_generator, string_types): + i = [text_or_generator] + else: + i = iter(text_or_generator) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, string_types) else text_type(el) for el in i) + + from ._termui_impl import pager + + return pager(itertools.chain(text_generator, "\n"), color) + + +def progressbar( + iterable=None, + length=None, + label=None, + show_eta=True, + show_percent=None, + show_pos=False, + item_show_func=None, + fill_char="#", + empty_char="-", + bar_template="%(label)s [%(bar)s] %(info)s", + info_sep=" ", + width=36, + file=None, + color=None, +): + """This function creates an iterable context manager that can be used + to iterate over something while showing a progress bar. It will + either iterate over the `iterable` or `length` items (that are counted + up). While iteration happens, this function will print a rendered + progress bar to the given `file` (defaults to stdout) and will attempt + to calculate remaining time and more. By default, this progress bar + will not be rendered if the file is not a terminal. + + The context manager creates the progress bar. When the context + manager is entered the progress bar is already created. With every + iteration over the progress bar, the iterable passed to the bar is + advanced and the bar is updated. When the context manager exits, + a newline is printed and the progress bar is finalized on screen. + + Note: The progress bar is currently designed for use cases where the + total progress can be expected to take at least several seconds. + Because of this, the ProgressBar class object won't display + progress that is considered too fast, and progress where the time + between steps is less than a second. + + No printing must happen or the progress bar will be unintentionally + destroyed. + + Example usage:: + + with progressbar(items) as bar: + for item in bar: + do_something_with(item) + + Alternatively, if no iterable is specified, one can manually update the + progress bar through the `update()` method instead of directly + iterating over the progress bar. The update method accepts the number + of steps to increment the bar with:: + + with progressbar(length=chunks.total_bytes) as bar: + for chunk in chunks: + process_chunk(chunk) + bar.update(chunks.bytes) + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `color` parameter. Added a `update` method to the + progressbar object. + + :param iterable: an iterable to iterate over. If not provided the length + is required. + :param length: the number of items to iterate over. By default the + progressbar will attempt to ask the iterator about its + length, which might or might not work. If an iterable is + also provided this parameter can be used to override the + length. If an iterable is not provided the progress bar + will iterate over a range of that length. + :param label: the label to show next to the progress bar. + :param show_eta: enables or disables the estimated time display. This is + automatically disabled if the length cannot be + determined. + :param show_percent: enables or disables the percentage display. The + default is `True` if the iterable has a length or + `False` if not. + :param show_pos: enables or disables the absolute position display. The + default is `False`. + :param item_show_func: a function called with the current item which + can return a string to show the current item + next to the progress bar. Note that the current + item can be `None`! + :param fill_char: the character to use to show the filled part of the + progress bar. + :param empty_char: the character to use to show the non-filled part of + the progress bar. + :param bar_template: the format string to use as template for the bar. + The parameters in it are ``label`` for the label, + ``bar`` for the progress bar and ``info`` for the + info section. + :param info_sep: the separator between multiple info items (eta etc.) + :param width: the width of the progress bar in characters, 0 means full + terminal width + :param file: the file to write to. If this is not a terminal then + only the label is printed. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are included anywhere in the progress bar output + which is not the case by default. + """ + from ._termui_impl import ProgressBar + + color = resolve_color_default(color) + return ProgressBar( + iterable=iterable, + length=length, + show_eta=show_eta, + show_percent=show_percent, + show_pos=show_pos, + item_show_func=item_show_func, + fill_char=fill_char, + empty_char=empty_char, + bar_template=bar_template, + info_sep=info_sep, + file=file, + label=label, + width=width, + color=color, + ) + + +def clear(): + """Clears the terminal screen. This will have the effect of clearing + the whole visible space of the terminal and moving the cursor to the + top left. This does not do anything if not connected to a terminal. + + .. versionadded:: 2.0 + """ + if not isatty(sys.stdout): + return + # If we're on Windows and we don't have colorama available, then we + # clear the screen by shelling out. Otherwise we can use an escape + # sequence. + if WIN: + os.system("cls") + else: + sys.stdout.write("\033[2J\033[1;1H") + + +def style( + text, + fg=None, + bg=None, + bold=None, + dim=None, + underline=None, + blink=None, + reverse=None, + reset=True, +): + """Styles a text with ANSI styles and returns the new string. By + default the styling is self contained which means that at the end + of the string a reset code is issued. This can be prevented by + passing ``reset=False``. + + Examples:: + + click.echo(click.style('Hello World!', fg='green')) + click.echo(click.style('ATTENTION!', blink=True)) + click.echo(click.style('Some things', reverse=True, fg='cyan')) + + Supported color names: + + * ``black`` (might be a gray) + * ``red`` + * ``green`` + * ``yellow`` (might be an orange) + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` + * ``reset`` (reset the color code only) + + .. versionadded:: 2.0 + + .. versionadded:: 7.0 + Added support for bright colors. + + :param text: the string to style with ansi codes. + :param fg: if provided this will become the foreground color. + :param bg: if provided this will become the background color. + :param bold: if provided this will enable or disable bold mode. + :param dim: if provided this will enable or disable dim mode. This is + badly supported. + :param underline: if provided this will enable or disable underline. + :param blink: if provided this will enable or disable blinking. + :param reverse: if provided this will enable or disable inverse + rendering (foreground becomes background and the + other way round). + :param reset: by default a reset-all code is added at the end of the + string which means that styles do not carry over. This + can be disabled to compose styles. + """ + bits = [] + if fg: + try: + bits.append("\033[{}m".format(_ansi_colors[fg])) + except KeyError: + raise TypeError("Unknown color '{}'".format(fg)) + if bg: + try: + bits.append("\033[{}m".format(_ansi_colors[bg] + 10)) + except KeyError: + raise TypeError("Unknown color '{}'".format(bg)) + if bold is not None: + bits.append("\033[{}m".format(1 if bold else 22)) + if dim is not None: + bits.append("\033[{}m".format(2 if dim else 22)) + if underline is not None: + bits.append("\033[{}m".format(4 if underline else 24)) + if blink is not None: + bits.append("\033[{}m".format(5 if blink else 25)) + if reverse is not None: + bits.append("\033[{}m".format(7 if reverse else 27)) + bits.append(text) + if reset: + bits.append(_ansi_reset_all) + return "".join(bits) + + +def unstyle(text): + """Removes ANSI styling information from a string. Usually it's not + necessary to use this function as Click's echo function will + automatically remove styling if necessary. + + .. versionadded:: 2.0 + + :param text: the text to remove style information from. + """ + return strip_ansi(text) + + +def secho(message=None, file=None, nl=True, err=False, color=None, **styles): + """This function combines :func:`echo` and :func:`style` into one + call. As such the following two calls are the same:: + + click.secho('Hello World!', fg='green') + click.echo(click.style('Hello World!', fg='green')) + + All keyword arguments are forwarded to the underlying functions + depending on which one they go with. + + .. versionadded:: 2.0 + """ + if message is not None: + message = style(message, **styles) + return echo(message, file=file, nl=nl, err=err, color=color) + + +def edit( + text=None, editor=None, env=None, require_save=True, extension=".txt", filename=None +): + r"""Edits the given text in the defined editor. If an editor is given + (should be the full path to the executable but the regular operating + system search path is used for finding the executable) it overrides + the detected editor. Optionally, some environment variables can be + used. If the editor is closed without changes, `None` is returned. In + case a file is edited directly the return value is always `None` and + `require_save` and `extension` are ignored. + + If the editor cannot be opened a :exc:`UsageError` is raised. + + Note for Windows: to simplify cross-platform usage, the newlines are + automatically converted from POSIX to Windows and vice versa. As such, + the message here will have ``\n`` as newline markers. + + :param text: the text to edit. + :param editor: optionally the editor to use. Defaults to automatic + detection. + :param env: environment variables to forward to the editor. + :param require_save: if this is true, then not saving in the editor + will make the return value become `None`. + :param extension: the extension to tell the editor about. This defaults + to `.txt` but changing this might change syntax + highlighting. + :param filename: if provided it will edit this file instead of the + provided text contents. It will not use a temporary + file as an indirection in that case. + """ + from ._termui_impl import Editor + + editor = Editor( + editor=editor, env=env, require_save=require_save, extension=extension + ) + if filename is None: + return editor.edit(text) + editor.edit_file(filename) + + +def launch(url, wait=False, locate=False): + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + Examples:: + + click.launch('https://click.palletsprojects.com/') + click.launch('/my/downloaded/file', locate=True) + + .. versionadded:: 2.0 + + :param url: URL or filename of the thing to launch. + :param wait: waits for the program to stop. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + from ._termui_impl import open_url + + return open_url(url, wait=wait, locate=locate) + + +# If this is provided, getchar() calls into this instead. This is used +# for unittesting purposes. +_getchar = None + + +def getchar(echo=False): + """Fetches a single character from the terminal and returns it. This + will always return a unicode character and under certain rare + circumstances this might return more than one character. The + situations which more than one character is returned is when for + whatever reason multiple characters end up in the terminal buffer or + standard input was not actually a terminal. + + Note that this will always read from the terminal, even if something + is piped into the standard input. + + Note for Windows: in rare cases when typing non-ASCII characters, this + function might wait for a second character and then return both at once. + This is because certain Unicode characters look like special-key markers. + + .. versionadded:: 2.0 + + :param echo: if set to `True`, the character read will also show up on + the terminal. The default is to not show it. + """ + f = _getchar + if f is None: + from ._termui_impl import getchar as f + return f(echo) + + +def raw_terminal(): + from ._termui_impl import raw_terminal as f + + return f() + + +def pause(info="Press any key to continue ...", err=False): + """This command stops execution and waits for the user to press any + key to continue. This is similar to the Windows batch "pause" + command. If the program is not run through a terminal, this command + will instead do nothing. + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param info: the info string to print before pausing. + :param err: if set to message goes to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + if not isatty(sys.stdin) or not isatty(sys.stdout): + return + try: + if info: + echo(info, nl=False, err=err) + try: + getchar() + except (KeyboardInterrupt, EOFError): + pass + finally: + if info: + echo(err=err) diff --git a/openpype/vendor/python/python_2/click/testing.py b/openpype/vendor/python/python_2/click/testing.py new file mode 100644 index 0000000000..a3dba3b301 --- /dev/null +++ b/openpype/vendor/python/python_2/click/testing.py @@ -0,0 +1,382 @@ +import contextlib +import os +import shlex +import shutil +import sys +import tempfile + +from . import formatting +from . import termui +from . import utils +from ._compat import iteritems +from ._compat import PY2 +from ._compat import string_types + + +if PY2: + from cStringIO import StringIO +else: + import io + from ._compat import _find_binary_reader + + +class EchoingStdin(object): + def __init__(self, input, output): + self._input = input + self._output = output + + def __getattr__(self, x): + return getattr(self._input, x) + + def _echo(self, rv): + self._output.write(rv) + return rv + + def read(self, n=-1): + return self._echo(self._input.read(n)) + + def readline(self, n=-1): + return self._echo(self._input.readline(n)) + + def readlines(self): + return [self._echo(x) for x in self._input.readlines()] + + def __iter__(self): + return iter(self._echo(x) for x in self._input) + + def __repr__(self): + return repr(self._input) + + +def make_input_stream(input, charset): + # Is already an input stream. + if hasattr(input, "read"): + if PY2: + return input + rv = _find_binary_reader(input) + if rv is not None: + return rv + raise TypeError("Could not find binary reader for input stream.") + + if input is None: + input = b"" + elif not isinstance(input, bytes): + input = input.encode(charset) + if PY2: + return StringIO(input) + return io.BytesIO(input) + + +class Result(object): + """Holds the captured result of an invoked CLI script.""" + + def __init__( + self, runner, stdout_bytes, stderr_bytes, exit_code, exception, exc_info=None + ): + #: The runner that created the result + self.runner = runner + #: The standard output as bytes. + self.stdout_bytes = stdout_bytes + #: The standard error as bytes, or None if not available + self.stderr_bytes = stderr_bytes + #: The exit code as integer. + self.exit_code = exit_code + #: The exception that happened if one did. + self.exception = exception + #: The traceback + self.exc_info = exc_info + + @property + def output(self): + """The (standard) output as unicode string.""" + return self.stdout + + @property + def stdout(self): + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stderr(self): + """The standard error as unicode string.""" + if self.stderr_bytes is None: + raise ValueError("stderr not separately captured") + return self.stderr_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + def __repr__(self): + return "<{} {}>".format( + type(self).__name__, repr(self.exception) if self.exception else "okay" + ) + + +class CliRunner(object): + """The CLI runner provides functionality to invoke a Click command line + script for unittesting purposes in a isolated environment. This only + works in single-threaded systems without any concurrency as it changes the + global interpreter state. + + :param charset: the character set for the input and output data. This is + UTF-8 by default and should not be changed currently as + the reporting to Click only works in Python 2 properly. + :param env: a dictionary with environment variables for overriding. + :param echo_stdin: if this is set to `True`, then reading from stdin writes + to stdout. This is useful for showing examples in + some circumstances. Note that regular prompts + will automatically echo the input. + :param mix_stderr: if this is set to `False`, then stdout and stderr are + preserved as independent streams. This is useful for + Unix-philosophy apps that have predictable stdout and + noisy stderr, such that each may be measured + independently + """ + + def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True): + if charset is None: + charset = "utf-8" + self.charset = charset + self.env = env or {} + self.echo_stdin = echo_stdin + self.mix_stderr = mix_stderr + + def get_default_prog_name(self, cli): + """Given a command object it will return the default program name + for it. The default is the `name` attribute or ``"root"`` if not + set. + """ + return cli.name or "root" + + def make_env(self, overrides=None): + """Returns the environment overrides for invoking a script.""" + rv = dict(self.env) + if overrides: + rv.update(overrides) + return rv + + @contextlib.contextmanager + def isolation(self, input=None, env=None, color=False): + """A context manager that sets up the isolation for invoking of a + command line tool. This sets up stdin with the given input data + and `os.environ` with the overrides from the given dictionary. + This also rebinds some internals in Click to be mocked (like the + prompt functionality). + + This is automatically done in the :meth:`invoke` method. + + .. versionadded:: 4.0 + The ``color`` parameter was added. + + :param input: the input stream to put into sys.stdin. + :param env: the environment overrides as dictionary. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + """ + input = make_input_stream(input, self.charset) + + old_stdin = sys.stdin + old_stdout = sys.stdout + old_stderr = sys.stderr + old_forced_width = formatting.FORCED_WIDTH + formatting.FORCED_WIDTH = 80 + + env = self.make_env(env) + + if PY2: + bytes_output = StringIO() + if self.echo_stdin: + input = EchoingStdin(input, bytes_output) + sys.stdout = bytes_output + if not self.mix_stderr: + bytes_error = StringIO() + sys.stderr = bytes_error + else: + bytes_output = io.BytesIO() + if self.echo_stdin: + input = EchoingStdin(input, bytes_output) + input = io.TextIOWrapper(input, encoding=self.charset) + sys.stdout = io.TextIOWrapper(bytes_output, encoding=self.charset) + if not self.mix_stderr: + bytes_error = io.BytesIO() + sys.stderr = io.TextIOWrapper(bytes_error, encoding=self.charset) + + if self.mix_stderr: + sys.stderr = sys.stdout + + sys.stdin = input + + def visible_input(prompt=None): + sys.stdout.write(prompt or "") + val = input.readline().rstrip("\r\n") + sys.stdout.write("{}\n".format(val)) + sys.stdout.flush() + return val + + def hidden_input(prompt=None): + sys.stdout.write("{}\n".format(prompt or "")) + sys.stdout.flush() + return input.readline().rstrip("\r\n") + + def _getchar(echo): + char = sys.stdin.read(1) + if echo: + sys.stdout.write(char) + sys.stdout.flush() + return char + + default_color = color + + def should_strip_ansi(stream=None, color=None): + if color is None: + return not default_color + return not color + + old_visible_prompt_func = termui.visible_prompt_func + old_hidden_prompt_func = termui.hidden_prompt_func + old__getchar_func = termui._getchar + old_should_strip_ansi = utils.should_strip_ansi + termui.visible_prompt_func = visible_input + termui.hidden_prompt_func = hidden_input + termui._getchar = _getchar + utils.should_strip_ansi = should_strip_ansi + + old_env = {} + try: + for key, value in iteritems(env): + old_env[key] = os.environ.get(key) + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + yield (bytes_output, not self.mix_stderr and bytes_error) + finally: + for key, value in iteritems(old_env): + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + sys.stdout = old_stdout + sys.stderr = old_stderr + sys.stdin = old_stdin + termui.visible_prompt_func = old_visible_prompt_func + termui.hidden_prompt_func = old_hidden_prompt_func + termui._getchar = old__getchar_func + utils.should_strip_ansi = old_should_strip_ansi + formatting.FORCED_WIDTH = old_forced_width + + def invoke( + self, + cli, + args=None, + input=None, + env=None, + catch_exceptions=True, + color=False, + **extra + ): + """Invokes a command in an isolated environment. The arguments are + forwarded directly to the command line script, the `extra` keyword + arguments are passed to the :meth:`~clickpkg.Command.main` function of + the command. + + This returns a :class:`Result` object. + + .. versionadded:: 3.0 + The ``catch_exceptions`` parameter was added. + + .. versionchanged:: 3.0 + The result object now has an `exc_info` attribute with the + traceback if available. + + .. versionadded:: 4.0 + The ``color`` parameter was added. + + :param cli: the command to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. + :param input: the input data for `sys.stdin`. + :param env: the environment overrides. + :param catch_exceptions: Whether to catch any other exceptions than + ``SystemExit``. + :param extra: the keyword arguments to pass to :meth:`main`. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + """ + exc_info = None + with self.isolation(input=input, env=env, color=color) as outstreams: + exception = None + exit_code = 0 + + if isinstance(args, string_types): + args = shlex.split(args) + + try: + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + cli.main(args=args or (), prog_name=prog_name, **extra) + except SystemExit as e: + exc_info = sys.exc_info() + exit_code = e.code + if exit_code is None: + exit_code = 0 + + if exit_code != 0: + exception = e + + if not isinstance(exit_code, int): + sys.stdout.write(str(exit_code)) + sys.stdout.write("\n") + exit_code = 1 + + except Exception as e: + if not catch_exceptions: + raise + exception = e + exit_code = 1 + exc_info = sys.exc_info() + finally: + sys.stdout.flush() + stdout = outstreams[0].getvalue() + if self.mix_stderr: + stderr = None + else: + stderr = outstreams[1].getvalue() + + return Result( + runner=self, + stdout_bytes=stdout, + stderr_bytes=stderr, + exit_code=exit_code, + exception=exception, + exc_info=exc_info, + ) + + @contextlib.contextmanager + def isolated_filesystem(self): + """A context manager that creates a temporary folder and changes + the current working directory to it for isolated filesystem tests. + """ + cwd = os.getcwd() + t = tempfile.mkdtemp() + os.chdir(t) + try: + yield t + finally: + os.chdir(cwd) + try: + shutil.rmtree(t) + except (OSError, IOError): # noqa: B014 + pass diff --git a/openpype/vendor/python/python_2/click/types.py b/openpype/vendor/python/python_2/click/types.py new file mode 100644 index 0000000000..505c39f850 --- /dev/null +++ b/openpype/vendor/python/python_2/click/types.py @@ -0,0 +1,762 @@ +import os +import stat +from datetime import datetime + +from ._compat import _get_argv_encoding +from ._compat import filename_to_ui +from ._compat import get_filesystem_encoding +from ._compat import get_streerror +from ._compat import open_stream +from ._compat import PY2 +from ._compat import text_type +from .exceptions import BadParameter +from .utils import LazyFile +from .utils import safecall + + +class ParamType(object): + """Helper for converting values through types. The following is + necessary for a valid type: + + * it needs a name + * it needs to pass through None unchanged + * it needs to convert from a string + * it needs to convert its result type through unchanged + (eg: needs to be idempotent) + * it needs to be able to deal with param and context being `None`. + This can be the case when the object is used with prompt + inputs. + """ + + is_composite = False + + #: the descriptive name of this type + name = None + + #: if a list of this type is expected and the value is pulled from a + #: string environment variable, this is what splits it up. `None` + #: means any whitespace. For all parameters the general rule is that + #: whitespace splits them up. The exception are paths and files which + #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on + #: Windows). + envvar_list_splitter = None + + def __call__(self, value, param=None, ctx=None): + if value is not None: + return self.convert(value, param, ctx) + + def get_metavar(self, param): + """Returns the metavar default for this param if it provides one.""" + + def get_missing_message(self, param): + """Optionally might return extra information about a missing + parameter. + + .. versionadded:: 2.0 + """ + + def convert(self, value, param, ctx): + """Converts the value. This is not invoked for values that are + `None` (the missing value). + """ + return value + + def split_envvar_value(self, rv): + """Given a value from an environment variable this splits it up + into small chunks depending on the defined envvar list splitter. + + If the splitter is set to `None`, which means that whitespace splits, + then leading and trailing whitespace is ignored. Otherwise, leading + and trailing splitters usually lead to empty items being included. + """ + return (rv or "").split(self.envvar_list_splitter) + + def fail(self, message, param=None, ctx=None): + """Helper method to fail with an invalid value message.""" + raise BadParameter(message, ctx=ctx, param=param) + + +class CompositeParamType(ParamType): + is_composite = True + + @property + def arity(self): + raise NotImplementedError() + + +class FuncParamType(ParamType): + def __init__(self, func): + self.name = func.__name__ + self.func = func + + def convert(self, value, param, ctx): + try: + return self.func(value) + except ValueError: + try: + value = text_type(value) + except UnicodeError: + value = str(value).decode("utf-8", "replace") + self.fail(value, param, ctx) + + +class UnprocessedParamType(ParamType): + name = "text" + + def convert(self, value, param, ctx): + return value + + def __repr__(self): + return "UNPROCESSED" + + +class StringParamType(ParamType): + name = "text" + + def convert(self, value, param, ctx): + if isinstance(value, bytes): + enc = _get_argv_encoding() + try: + value = value.decode(enc) + except UnicodeError: + fs_enc = get_filesystem_encoding() + if fs_enc != enc: + try: + value = value.decode(fs_enc) + except UnicodeError: + value = value.decode("utf-8", "replace") + else: + value = value.decode("utf-8", "replace") + return value + return value + + def __repr__(self): + return "STRING" + + +class Choice(ParamType): + """The choice type allows a value to be checked against a fixed set + of supported values. All of these values have to be strings. + + You should only pass a list or tuple of choices. Other iterables + (like generators) may lead to surprising results. + + The resulting value will always be one of the originally passed choices + regardless of ``case_sensitive`` or any ``ctx.token_normalize_func`` + being specified. + + See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. + """ + + name = "choice" + + def __init__(self, choices, case_sensitive=True): + self.choices = choices + self.case_sensitive = case_sensitive + + def get_metavar(self, param): + return "[{}]".format("|".join(self.choices)) + + def get_missing_message(self, param): + return "Choose from:\n\t{}.".format(",\n\t".join(self.choices)) + + def convert(self, value, param, ctx): + # Match through normalization and case sensitivity + # first do token_normalize_func, then lowercase + # preserve original `value` to produce an accurate message in + # `self.fail` + normed_value = value + normed_choices = {choice: choice for choice in self.choices} + + if ctx is not None and ctx.token_normalize_func is not None: + normed_value = ctx.token_normalize_func(value) + normed_choices = { + ctx.token_normalize_func(normed_choice): original + for normed_choice, original in normed_choices.items() + } + + if not self.case_sensitive: + if PY2: + lower = str.lower + else: + lower = str.casefold + + normed_value = lower(normed_value) + normed_choices = { + lower(normed_choice): original + for normed_choice, original in normed_choices.items() + } + + if normed_value in normed_choices: + return normed_choices[normed_value] + + self.fail( + "invalid choice: {}. (choose from {})".format( + value, ", ".join(self.choices) + ), + param, + ctx, + ) + + def __repr__(self): + return "Choice('{}')".format(list(self.choices)) + + +class DateTime(ParamType): + """The DateTime type converts date strings into `datetime` objects. + + The format strings which are checked are configurable, but default to some + common (non-timezone aware) ISO 8601 formats. + + When specifying *DateTime* formats, you should only pass a list or a tuple. + Other iterables, like generators, may lead to surprising results. + + The format strings are processed using ``datetime.strptime``, and this + consequently defines the format strings which are allowed. + + Parsing is tried using each format, in order, and the first format which + parses successfully is used. + + :param formats: A list or tuple of date format strings, in the order in + which they should be tried. Defaults to + ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, + ``'%Y-%m-%d %H:%M:%S'``. + """ + + name = "datetime" + + def __init__(self, formats=None): + self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] + + def get_metavar(self, param): + return "[{}]".format("|".join(self.formats)) + + def _try_to_convert_date(self, value, format): + try: + return datetime.strptime(value, format) + except ValueError: + return None + + def convert(self, value, param, ctx): + # Exact match + for format in self.formats: + dtime = self._try_to_convert_date(value, format) + if dtime: + return dtime + + self.fail( + "invalid datetime format: {}. (choose from {})".format( + value, ", ".join(self.formats) + ) + ) + + def __repr__(self): + return "DateTime" + + +class IntParamType(ParamType): + name = "integer" + + def convert(self, value, param, ctx): + try: + return int(value) + except ValueError: + self.fail("{} is not a valid integer".format(value), param, ctx) + + def __repr__(self): + return "INT" + + +class IntRange(IntParamType): + """A parameter that works similar to :data:`click.INT` but restricts + the value to fit into a range. The default behavior is to fail if the + value falls outside the range, but it can also be silently clamped + between the two edges. + + See :ref:`ranges` for an example. + """ + + name = "integer range" + + def __init__(self, min=None, max=None, clamp=False): + self.min = min + self.max = max + self.clamp = clamp + + def convert(self, value, param, ctx): + rv = IntParamType.convert(self, value, param, ctx) + if self.clamp: + if self.min is not None and rv < self.min: + return self.min + if self.max is not None and rv > self.max: + return self.max + if ( + self.min is not None + and rv < self.min + or self.max is not None + and rv > self.max + ): + if self.min is None: + self.fail( + "{} is bigger than the maximum valid value {}.".format( + rv, self.max + ), + param, + ctx, + ) + elif self.max is None: + self.fail( + "{} is smaller than the minimum valid value {}.".format( + rv, self.min + ), + param, + ctx, + ) + else: + self.fail( + "{} is not in the valid range of {} to {}.".format( + rv, self.min, self.max + ), + param, + ctx, + ) + return rv + + def __repr__(self): + return "IntRange({}, {})".format(self.min, self.max) + + +class FloatParamType(ParamType): + name = "float" + + def convert(self, value, param, ctx): + try: + return float(value) + except ValueError: + self.fail( + "{} is not a valid floating point value".format(value), param, ctx + ) + + def __repr__(self): + return "FLOAT" + + +class FloatRange(FloatParamType): + """A parameter that works similar to :data:`click.FLOAT` but restricts + the value to fit into a range. The default behavior is to fail if the + value falls outside the range, but it can also be silently clamped + between the two edges. + + See :ref:`ranges` for an example. + """ + + name = "float range" + + def __init__(self, min=None, max=None, clamp=False): + self.min = min + self.max = max + self.clamp = clamp + + def convert(self, value, param, ctx): + rv = FloatParamType.convert(self, value, param, ctx) + if self.clamp: + if self.min is not None and rv < self.min: + return self.min + if self.max is not None and rv > self.max: + return self.max + if ( + self.min is not None + and rv < self.min + or self.max is not None + and rv > self.max + ): + if self.min is None: + self.fail( + "{} is bigger than the maximum valid value {}.".format( + rv, self.max + ), + param, + ctx, + ) + elif self.max is None: + self.fail( + "{} is smaller than the minimum valid value {}.".format( + rv, self.min + ), + param, + ctx, + ) + else: + self.fail( + "{} is not in the valid range of {} to {}.".format( + rv, self.min, self.max + ), + param, + ctx, + ) + return rv + + def __repr__(self): + return "FloatRange({}, {})".format(self.min, self.max) + + +class BoolParamType(ParamType): + name = "boolean" + + def convert(self, value, param, ctx): + if isinstance(value, bool): + return bool(value) + value = value.lower() + if value in ("true", "t", "1", "yes", "y"): + return True + elif value in ("false", "f", "0", "no", "n"): + return False + self.fail("{} is not a valid boolean".format(value), param, ctx) + + def __repr__(self): + return "BOOL" + + +class UUIDParameterType(ParamType): + name = "uuid" + + def convert(self, value, param, ctx): + import uuid + + try: + if PY2 and isinstance(value, text_type): + value = value.encode("ascii") + return uuid.UUID(value) + except ValueError: + self.fail("{} is not a valid UUID value".format(value), param, ctx) + + def __repr__(self): + return "UUID" + + +class File(ParamType): + """Declares a parameter to be a file for reading or writing. The file + is automatically closed once the context tears down (after the command + finished working). + + Files can be opened for reading or writing. The special value ``-`` + indicates stdin or stdout depending on the mode. + + By default, the file is opened for reading text data, but it can also be + opened in binary mode or for writing. The encoding parameter can be used + to force a specific encoding. + + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. + + Starting with Click 2.0, files can also be opened atomically in which + case all writes go into a separate file in the same folder and upon + completion the file will be moved over to the original location. This + is useful if a file regularly read by other users is modified. + + See :ref:`file-args` for more information. + """ + + name = "filename" + envvar_list_splitter = os.path.pathsep + + def __init__( + self, mode="r", encoding=None, errors="strict", lazy=None, atomic=False + ): + self.mode = mode + self.encoding = encoding + self.errors = errors + self.lazy = lazy + self.atomic = atomic + + def resolve_lazy_flag(self, value): + if self.lazy is not None: + return self.lazy + if value == "-": + return False + elif "w" in self.mode: + return True + return False + + def convert(self, value, param, ctx): + try: + if hasattr(value, "read") or hasattr(value, "write"): + return value + + lazy = self.resolve_lazy_flag(value) + + if lazy: + f = LazyFile( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + if ctx is not None: + ctx.call_on_close(f.close_intelligently) + return f + + f, should_close = open_stream( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + # If a context is provided, we automatically close the file + # at the end of the context execution (or flush out). If a + # context does not exist, it's the caller's responsibility to + # properly close the file. This for instance happens when the + # type is used with prompts. + if ctx is not None: + if should_close: + ctx.call_on_close(safecall(f.close)) + else: + ctx.call_on_close(safecall(f.flush)) + return f + except (IOError, OSError) as e: # noqa: B014 + self.fail( + "Could not open file: {}: {}".format( + filename_to_ui(value), get_streerror(e) + ), + param, + ctx, + ) + + +class Path(ParamType): + """The path type is similar to the :class:`File` type but it performs + different checks. First of all, instead of returning an open file + handle it returns just the filename. Secondly, it can perform various + basic checks about what the file or directory should be. + + .. versionchanged:: 6.0 + `allow_dash` was added. + + :param exists: if set to true, the file or directory needs to exist for + this value to be valid. If this is not required and a + file does indeed not exist, then all further checks are + silently skipped. + :param file_okay: controls if a file is a possible value. + :param dir_okay: controls if a directory is a possible value. + :param writable: if true, a writable check is performed. + :param readable: if true, a readable check is performed. + :param resolve_path: if this is true, then the path is fully resolved + before the value is passed onwards. This means + that it's absolute and symlinks are resolved. It + will not expand a tilde-prefix, as this is + supposed to be done by the shell only. + :param allow_dash: If this is set to `True`, a single dash to indicate + standard streams is permitted. + :param path_type: optionally a string type that should be used to + represent the path. The default is `None` which + means the return value will be either bytes or + unicode depending on what makes most sense given the + input data Click deals with. + """ + + envvar_list_splitter = os.path.pathsep + + def __init__( + self, + exists=False, + file_okay=True, + dir_okay=True, + writable=False, + readable=True, + resolve_path=False, + allow_dash=False, + path_type=None, + ): + self.exists = exists + self.file_okay = file_okay + self.dir_okay = dir_okay + self.writable = writable + self.readable = readable + self.resolve_path = resolve_path + self.allow_dash = allow_dash + self.type = path_type + + if self.file_okay and not self.dir_okay: + self.name = "file" + self.path_type = "File" + elif self.dir_okay and not self.file_okay: + self.name = "directory" + self.path_type = "Directory" + else: + self.name = "path" + self.path_type = "Path" + + def coerce_path_result(self, rv): + if self.type is not None and not isinstance(rv, self.type): + if self.type is text_type: + rv = rv.decode(get_filesystem_encoding()) + else: + rv = rv.encode(get_filesystem_encoding()) + return rv + + def convert(self, value, param, ctx): + rv = value + + is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-") + + if not is_dash: + if self.resolve_path: + rv = os.path.realpath(rv) + + try: + st = os.stat(rv) + except OSError: + if not self.exists: + return self.coerce_path_result(rv) + self.fail( + "{} '{}' does not exist.".format( + self.path_type, filename_to_ui(value) + ), + param, + ctx, + ) + + if not self.file_okay and stat.S_ISREG(st.st_mode): + self.fail( + "{} '{}' is a file.".format(self.path_type, filename_to_ui(value)), + param, + ctx, + ) + if not self.dir_okay and stat.S_ISDIR(st.st_mode): + self.fail( + "{} '{}' is a directory.".format( + self.path_type, filename_to_ui(value) + ), + param, + ctx, + ) + if self.writable and not os.access(value, os.W_OK): + self.fail( + "{} '{}' is not writable.".format( + self.path_type, filename_to_ui(value) + ), + param, + ctx, + ) + if self.readable and not os.access(value, os.R_OK): + self.fail( + "{} '{}' is not readable.".format( + self.path_type, filename_to_ui(value) + ), + param, + ctx, + ) + + return self.coerce_path_result(rv) + + +class Tuple(CompositeParamType): + """The default behavior of Click is to apply a type on a value directly. + This works well in most cases, except for when `nargs` is set to a fixed + count and different types should be used for different items. In this + case the :class:`Tuple` type can be used. This type can only be used + if `nargs` is set to a fixed number. + + For more information see :ref:`tuple-type`. + + This can be selected by using a Python tuple literal as a type. + + :param types: a list of types that should be used for the tuple items. + """ + + def __init__(self, types): + self.types = [convert_type(ty) for ty in types] + + @property + def name(self): + return "<{}>".format(" ".join(ty.name for ty in self.types)) + + @property + def arity(self): + return len(self.types) + + def convert(self, value, param, ctx): + if len(value) != len(self.types): + raise TypeError( + "It would appear that nargs is set to conflict with the" + " composite type arity." + ) + return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value)) + + +def convert_type(ty, default=None): + """Converts a callable or python type into the most appropriate + param type. + """ + guessed_type = False + if ty is None and default is not None: + if isinstance(default, tuple): + ty = tuple(map(type, default)) + else: + ty = type(default) + guessed_type = True + + if isinstance(ty, tuple): + return Tuple(ty) + if isinstance(ty, ParamType): + return ty + if ty is text_type or ty is str or ty is None: + return STRING + if ty is int: + return INT + # Booleans are only okay if not guessed. This is done because for + # flags the default value is actually a bit of a lie in that it + # indicates which of the flags is the one we want. See get_default() + # for more information. + if ty is bool and not guessed_type: + return BOOL + if ty is float: + return FLOAT + if guessed_type: + return STRING + + # Catch a common mistake + if __debug__: + try: + if issubclass(ty, ParamType): + raise AssertionError( + "Attempted to use an uninstantiated parameter type ({}).".format(ty) + ) + except TypeError: + pass + return FuncParamType(ty) + + +#: A dummy parameter type that just does nothing. From a user's +#: perspective this appears to just be the same as `STRING` but internally +#: no string conversion takes place. This is necessary to achieve the +#: same bytes/unicode behavior on Python 2/3 in situations where you want +#: to not convert argument types. This is usually useful when working +#: with file paths as they can appear in bytes and unicode. +#: +#: For path related uses the :class:`Path` type is a better choice but +#: there are situations where an unprocessed type is useful which is why +#: it is is provided. +#: +#: .. versionadded:: 4.0 +UNPROCESSED = UnprocessedParamType() + +#: A unicode string parameter type which is the implicit default. This +#: can also be selected by using ``str`` as type. +STRING = StringParamType() + +#: An integer parameter. This can also be selected by using ``int`` as +#: type. +INT = IntParamType() + +#: A floating point value parameter. This can also be selected by using +#: ``float`` as type. +FLOAT = FloatParamType() + +#: A boolean parameter. This is the default for boolean flags. This can +#: also be selected by using ``bool`` as a type. +BOOL = BoolParamType() + +#: A UUID parameter. +UUID = UUIDParameterType() diff --git a/openpype/vendor/python/python_2/click/utils.py b/openpype/vendor/python/python_2/click/utils.py new file mode 100644 index 0000000000..79265e732d --- /dev/null +++ b/openpype/vendor/python/python_2/click/utils.py @@ -0,0 +1,455 @@ +import os +import sys + +from ._compat import _default_text_stderr +from ._compat import _default_text_stdout +from ._compat import auto_wrap_for_ansi +from ._compat import binary_streams +from ._compat import filename_to_ui +from ._compat import get_filesystem_encoding +from ._compat import get_streerror +from ._compat import is_bytes +from ._compat import open_stream +from ._compat import PY2 +from ._compat import should_strip_ansi +from ._compat import string_types +from ._compat import strip_ansi +from ._compat import text_streams +from ._compat import text_type +from ._compat import WIN +from .globals import resolve_color_default + +if not PY2: + from ._compat import _find_binary_writer +elif WIN: + from ._winconsole import _get_windows_argv + from ._winconsole import _hash_py_argv + from ._winconsole import _initial_argv_hash + +echo_native_types = string_types + (bytes, bytearray) + + +def _posixify(name): + return "-".join(name.split()).lower() + + +def safecall(func): + """Wraps a function so that it swallows exceptions.""" + + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: + pass + + return wrapper + + +def make_str(value): + """Converts a value into a valid string.""" + if isinstance(value, bytes): + try: + return value.decode(get_filesystem_encoding()) + except UnicodeError: + return value.decode("utf-8", "replace") + return text_type(value) + + +def make_default_short_help(help, max_length=45): + """Return a condensed version of help string.""" + words = help.split() + total_length = 0 + result = [] + done = False + + for word in words: + if word[-1:] == ".": + done = True + new_length = 1 + len(word) if result else len(word) + if total_length + new_length > max_length: + result.append("...") + done = True + else: + if result: + result.append(" ") + result.append(word) + if done: + break + total_length += new_length + + return "".join(result) + + +class LazyFile(object): + """A lazy file works like a regular file but it does not fully open + the file but it does perform some basic checks early to see if the + filename parameter does make sense. This is useful for safely opening + files for writing. + """ + + def __init__( + self, filename, mode="r", encoding=None, errors="strict", atomic=False + ): + self.name = filename + self.mode = mode + self.encoding = encoding + self.errors = errors + self.atomic = atomic + + if filename == "-": + self._f, self.should_close = open_stream(filename, mode, encoding, errors) + else: + if "r" in mode: + # Open and close the file in case we're opening it for + # reading so that we can catch at least some errors in + # some cases early. + open(filename, mode).close() + self._f = None + self.should_close = True + + def __getattr__(self, name): + return getattr(self.open(), name) + + def __repr__(self): + if self._f is not None: + return repr(self._f) + return "".format(self.name, self.mode) + + def open(self): + """Opens the file if it's not yet open. This call might fail with + a :exc:`FileError`. Not handling this error will produce an error + that Click shows. + """ + if self._f is not None: + return self._f + try: + rv, self.should_close = open_stream( + self.name, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + except (IOError, OSError) as e: # noqa: E402 + from .exceptions import FileError + + raise FileError(self.name, hint=get_streerror(e)) + self._f = rv + return rv + + def close(self): + """Closes the underlying file, no matter what.""" + if self._f is not None: + self._f.close() + + def close_intelligently(self): + """This function only closes the file if it was opened by the lazy + file wrapper. For instance this will never close stdin. + """ + if self.should_close: + self.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + self.close_intelligently() + + def __iter__(self): + self.open() + return iter(self._f) + + +class KeepOpenFile(object): + def __init__(self, file): + self._file = file + + def __getattr__(self, name): + return getattr(self._file, name) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + pass + + def __repr__(self): + return repr(self._file) + + def __iter__(self): + return iter(self._file) + + +def echo(message=None, file=None, nl=True, err=False, color=None): + """Prints a message plus a newline to the given file or stdout. On + first sight, this looks like the print function, but it has improved + support for handling Unicode and binary data that does not fail no + matter how badly configured the system is. + + Primarily it means that you can print binary data as well as Unicode + data on both 2.x and 3.x to the given file in the most appropriate way + possible. This is a very carefree function in that it will try its + best to not fail. As of Click 6.0 this includes support for unicode + output on the Windows console. + + In addition to that, if `colorama`_ is installed, the echo function will + also support clever handling of ANSI codes. Essentially it will then + do the following: + + - add transparent handling of ANSI color codes on Windows. + - hide ANSI codes automatically if the destination file is not a + terminal. + + .. _colorama: https://pypi.org/project/colorama/ + + .. versionchanged:: 6.0 + As of Click 6.0 the echo function will properly support unicode + output on the windows console. Not that click does not modify + the interpreter in any way which means that `sys.stdout` or the + print statement or function will still not provide unicode support. + + .. versionchanged:: 2.0 + Starting with version 2.0 of Click, the echo function will work + with colorama if it's installed. + + .. versionadded:: 3.0 + The `err` parameter was added. + + .. versionchanged:: 4.0 + Added the `color` flag. + + :param message: the message to print + :param file: the file to write to (defaults to ``stdout``) + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``. This is faster and easier than calling + :func:`get_text_stderr` yourself. + :param nl: if set to `True` (the default) a newline is printed afterwards. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. + """ + if file is None: + if err: + file = _default_text_stderr() + else: + file = _default_text_stdout() + + # Convert non bytes/text into the native string type. + if message is not None and not isinstance(message, echo_native_types): + message = text_type(message) + + if nl: + message = message or u"" + if isinstance(message, text_type): + message += u"\n" + else: + message += b"\n" + + # If there is a message, and we're in Python 3, and the value looks + # like bytes, we manually need to find the binary stream and write the + # message in there. This is done separately so that most stream + # types will work as you would expect. Eg: you can write to StringIO + # for other cases. + if message and not PY2 and is_bytes(message): + binary_file = _find_binary_writer(file) + if binary_file is not None: + file.flush() + binary_file.write(message) + binary_file.flush() + return + + # ANSI-style support. If there is no message or we are dealing with + # bytes nothing is happening. If we are connected to a file we want + # to strip colors. If we are on windows we either wrap the stream + # to strip the color or we use the colorama support to translate the + # ansi codes to API calls. + if message and not is_bytes(message): + color = resolve_color_default(color) + if should_strip_ansi(file, color): + message = strip_ansi(message) + elif WIN: + if auto_wrap_for_ansi is not None: + file = auto_wrap_for_ansi(file) + elif not color: + message = strip_ansi(message) + + if message: + file.write(message) + file.flush() + + +def get_binary_stream(name): + """Returns a system stream for byte processing. This essentially + returns the stream from the sys module with the given name but it + solves some compatibility issues between different Python versions. + Primarily this function is necessary for getting binary streams on + Python 3. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + """ + opener = binary_streams.get(name) + if opener is None: + raise TypeError("Unknown standard stream '{}'".format(name)) + return opener() + + +def get_text_stream(name, encoding=None, errors="strict"): + """Returns a system stream for text processing. This usually returns + a wrapped stream around a binary stream returned from + :func:`get_binary_stream` but it also can take shortcuts on Python 3 + for already correctly configured streams. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + :param encoding: overrides the detected default encoding. + :param errors: overrides the default error mode. + """ + opener = text_streams.get(name) + if opener is None: + raise TypeError("Unknown standard stream '{}'".format(name)) + return opener(encoding, errors) + + +def open_file( + filename, mode="r", encoding=None, errors="strict", lazy=False, atomic=False +): + """This is similar to how the :class:`File` works but for manual + usage. Files are opened non lazy by default. This can open regular + files as well as stdin/stdout if ``'-'`` is passed. + + If stdin/stdout is returned the stream is wrapped so that the context + manager will not close the stream accidentally. This makes it possible + to always use the function like this without having to worry to + accidentally close a standard stream:: + + with open_file(filename) as f: + ... + + .. versionadded:: 3.0 + + :param filename: the name of the file to open (or ``'-'`` for stdin/stdout). + :param mode: the mode in which to open the file. + :param encoding: the encoding to use. + :param errors: the error handling for this file. + :param lazy: can be flipped to true to open the file lazily. + :param atomic: in atomic mode writes go into a temporary file and it's + moved on close. + """ + if lazy: + return LazyFile(filename, mode, encoding, errors, atomic=atomic) + f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) + if not should_close: + f = KeepOpenFile(f) + return f + + +def get_os_args(): + """This returns the argument part of sys.argv in the most appropriate + form for processing. What this means is that this return value is in + a format that works for Click to process but does not necessarily + correspond well to what's actually standard for the interpreter. + + On most environments the return value is ``sys.argv[:1]`` unchanged. + However if you are on Windows and running Python 2 the return value + will actually be a list of unicode strings instead because the + default behavior on that platform otherwise will not be able to + carry all possible values that sys.argv can have. + + .. versionadded:: 6.0 + """ + # We can only extract the unicode argv if sys.argv has not been + # changed since the startup of the application. + if PY2 and WIN and _initial_argv_hash == _hash_py_argv(): + return _get_windows_argv() + return sys.argv[1:] + + +def format_filename(filename, shorten=False): + """Formats a filename for user display. The main purpose of this + function is to ensure that the filename can be displayed at all. This + will decode the filename to unicode if necessary in a way that it will + not fail. Optionally, it can shorten the filename to not include the + full path to the filename. + + :param filename: formats a filename for UI display. This will also convert + the filename into unicode without failing. + :param shorten: this optionally shortens the filename to strip of the + path that leads up to it. + """ + if shorten: + filename = os.path.basename(filename) + return filename_to_ui(filename) + + +def get_app_dir(app_name, roaming=True, force_posix=False): + r"""Returns the config folder for the application. The default behavior + is to return whatever is most appropriate for the operating system. + + To give you an idea, for an app called ``"Foo Bar"``, something like + the following folders could be returned: + + Mac OS X: + ``~/Library/Application Support/Foo Bar`` + Mac OS X (POSIX): + ``~/.foo-bar`` + Unix: + ``~/.config/foo-bar`` + Unix (POSIX): + ``~/.foo-bar`` + Win XP (roaming): + ``C:\Documents and Settings\\Local Settings\Application Data\Foo Bar`` + Win XP (not roaming): + ``C:\Documents and Settings\\Application Data\Foo Bar`` + Win 7 (roaming): + ``C:\Users\\AppData\Roaming\Foo Bar`` + Win 7 (not roaming): + ``C:\Users\\AppData\Local\Foo Bar`` + + .. versionadded:: 2.0 + + :param app_name: the application name. This should be properly capitalized + and can contain whitespace. + :param roaming: controls if the folder should be roaming or not on Windows. + Has no affect otherwise. + :param force_posix: if this is set to `True` then on any POSIX system the + folder will be stored in the home folder with a leading + dot instead of the XDG config home or darwin's + application support folder. + """ + if WIN: + key = "APPDATA" if roaming else "LOCALAPPDATA" + folder = os.environ.get(key) + if folder is None: + folder = os.path.expanduser("~") + return os.path.join(folder, app_name) + if force_posix: + return os.path.join(os.path.expanduser("~/.{}".format(_posixify(app_name)))) + if sys.platform == "darwin": + return os.path.join( + os.path.expanduser("~/Library/Application Support"), app_name + ) + return os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + _posixify(app_name), + ) + + +class PacifyFlushWrapper(object): + """This wrapper is used to catch and suppress BrokenPipeErrors resulting + from ``.flush()`` being called on broken pipe during the shutdown/final-GC + of the Python interpreter. Notably ``.flush()`` is always called on + ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any + other cleanup code, and the case where the underlying file is not a broken + pipe, all calls and attributes are proxied. + """ + + def __init__(self, wrapped): + self.wrapped = wrapped + + def flush(self): + try: + self.wrapped.flush() + except IOError as e: + import errno + + if e.errno != errno.EPIPE: + raise + + def __getattr__(self, attr): + return getattr(self.wrapped, attr) From a8ab471f84dfc8f87dc0b7a368132ca81cda156e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 1 Aug 2023 12:05:23 +0200 Subject: [PATCH 397/446] OP-4845 - removed unnecessary env var Used only when connecting to OP MongoDB. --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index f3e49efefd..5f7e1f1032 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -483,8 +483,6 @@ def inject_ayon_environment(deadlinePlugin): " AVALON_TASK, AVALON_APP_NAME" )) - os.environ["AVALON_TIMEOUT"] = "5000" - environment = { "AYON_SERVER_URL": ayon_server_url, "AYON_API_KEY": ayon_api_key, From 6c8d2f23072569ae9bec7a5ce648ccc977d6f9e0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Aug 2023 14:40:17 +0200 Subject: [PATCH 398/446] fixing sequence loading --- openpype/hosts/nuke/plugins/load/load_clip.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 5539324fb7..19038b168d 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -91,14 +91,14 @@ class LoadClip(plugin.NukeLoader): # reset container id so it is always unique for each instance self.reset_container_id() - self.log.warning(self.extensions) - is_sequence = len(representation["files"]) > 1 if is_sequence: - representation = self._representation_with_hash_in_frame( - representation + context["representation"] = \ + self._representation_with_hash_in_frame( + representation ) + filepath = self.filepath_from_context(context) filepath = filepath.replace("\\", "/") self.log.debug("_ filepath: {}".format(filepath)) @@ -260,6 +260,7 @@ class LoadClip(plugin.NukeLoader): representation = self._representation_with_hash_in_frame( representation ) + filepath = get_representation_path(representation).replace("\\", "/") self.log.debug("_ filepath: {}".format(filepath)) From d33d20b5485c70e037abab021f9da687234f8cbe Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 1 Aug 2023 15:37:57 +0200 Subject: [PATCH 399/446] Nuke: returned not cleaning of renders folder on the farm (#5374) * OP-6439 - mark farm rendered images for deletion only if not persistent Farm produces images into `renders` folder, which might be set as persistent for some hosts (Nuke). Mark rendered images for explicit deletion if they are not stored in persistent staging folder. * OP-6439 - allow storing of stagingDir_persistent into metadata.json Instance could carry `stagingDir_persistent` flag denoting that staging dir shouldnt be deleted. This allow to propagate this into farm publishing. TODO - shouldnt this be on representation as stagingDir is there and each repre could have different stagingDir? * OP-6439 - mark all Nuke staging dir as persistent Backward compatibility as previously Nuke kept images rendered in `renders` eg. stagingDir. There are workflows which rely on presence of files in `renders` folder. * Update openpype/pipeline/farm/pyblish_functions.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 6 ++++++ openpype/pipeline/farm/pyblish_functions.py | 3 +++ openpype/pipeline/publish/lib.py | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 2d1caacdc3..1eb1e1350f 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -193,4 +193,10 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, if not instance.data.get("review"): instance.data["useSequenceForReview"] = False + # TODO temporarily set stagingDir as persistent for backward + # compatibility. This is mainly focused on `renders`folders which + # were previously not cleaned up (and could be used in read notes) + # this logic should be removed and replaced with custom staging dir + instance.data["stagingDir_persistent"] = True + self.log.debug("instance.data: {}".format(pformat(instance.data))) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index e979c2d6ae..9278b0efc5 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -268,6 +268,9 @@ def create_skeleton_instance( instance_skeleton_data["representations"] = [] instance_skeleton_data["representations"] += representations + persistent = instance.data.get("stagingDir_persistent") is True + instance_skeleton_data["stagingDir_persistent"] = persistent + return instance_skeleton_data diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 2768fe3fa1..c14b6d2445 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -981,7 +981,7 @@ def add_repre_files_for_cleanup(instance, repre): """ files = repre["files"] staging_dir = repre.get("stagingDir") - if not staging_dir: + if not staging_dir or instance.data.get("stagingDir_persistent"): return if isinstance(files, str): From caa6a7d5f70c651d690041833ca82abb0b48ab61 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Aug 2023 16:39:21 +0200 Subject: [PATCH 400/446] cosmetics --- openpype/hosts/nuke/api/pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index fcc3becd2d..a48ae0032a 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -155,6 +155,7 @@ def add_nuke_callbacks(): """ nuke_settings = get_current_project_settings()["nuke"] workfile_settings = WorkfileSettings() + # Set context settings. nuke.addOnCreate( workfile_settings.set_context_settings, nodeClass="Root") @@ -173,7 +174,7 @@ def add_nuke_callbacks(): nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) if nuke_settings["nuke-dirmap"]["enabled"]: - log.info("Added Nuke's dirmaping callback ...") + log.info("Added Nuke's dir-mapping callback ...") # Add dirmap for file paths. nuke.addFilenameFilter(dirmap_file_name_filter) From 93e02e88553f9d693f2a391eef388af3482f712c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:55:15 +0200 Subject: [PATCH 401/446] AYON: Fix settings conversion for ayon addons (#5377) * fix settings conversion for ayon addons * Removed empty line --- openpype/settings/ayon_settings.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 90c7f33fd2..cd12a8f757 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -267,6 +267,7 @@ def _convert_modules_system( ): func(ayon_settings, output, addon_versions, default_settings) + modules_settings = output["modules"] for module_name in ( "sync_server", "log_viewer", @@ -279,7 +280,16 @@ def _convert_modules_system( settings = default_settings["modules"][module_name] if "enabled" in settings: settings["enabled"] = False - output["modules"][module_name] = settings + modules_settings[module_name] = settings + + for key, value in ayon_settings.items(): + if key not in output: + output[key] = value + + # Make sure addons have access to settings in initialization + # - ModulesManager passes only modules settings into initialization + if key not in modules_settings: + modules_settings[key] = value def convert_system_settings(ayon_settings, default_settings, addon_versions): @@ -293,15 +303,16 @@ def convert_system_settings(ayon_settings, default_settings, addon_versions): if "core" in ayon_settings: _convert_general(ayon_settings, output, default_settings) + for key, value in default_settings.items(): + if key not in output: + output[key] = value + _convert_modules_system( ayon_settings, output, addon_versions, default_settings ) - for key, value in default_settings.items(): - if key not in output: - output[key] = value return output From 6cb9779e6a745857c1d9cbc6bca1afea3c0124d6 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 2 Aug 2023 03:24:56 +0000 Subject: [PATCH 402/446] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 61bb0f8288..bbe452aeba 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.3-nightly.2" +__version__ = "3.16.3-nightly.3" From 31fc87a66784b6f608260be2666f79250a79f2c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 2 Aug 2023 03:25:40 +0000 Subject: [PATCH 403/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 387b5574ab..b6a243bcfe 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.3-nightly.3 - 3.16.3-nightly.2 - 3.16.3-nightly.1 - 3.16.2 @@ -134,7 +135,6 @@ body: - 3.14.7-nightly.4 - 3.14.7-nightly.3 - 3.14.7-nightly.2 - - 3.14.7-nightly.1 validations: required: true - type: dropdown From 48ac6c8ed1049b21920aaeee87cc143eb8f8bcb7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Aug 2023 10:09:11 +0200 Subject: [PATCH 404/446] define 'AYON_UNREAL_ROOT' environment variable in unreal addon --- openpype/hosts/unreal/addon.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 3225d742a3..fcc5d98ab6 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -12,6 +12,11 @@ class UnrealAddon(OpenPypeModule, IHostAddon): def initialize(self, module_settings): self.enabled = True + def get_global_environments(self): + return { + "AYON_UNREAL_ROOT": UNREAL_ROOT_DIR, + } + def add_implementation_envs(self, env, app): """Modify environments to contain all required for implementation.""" # Set AYON_UNREAL_PLUGIN required for Unreal implementation From ad82adbeca40a9cb8672916d20add28032b6405e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 2 Aug 2023 10:21:27 +0100 Subject: [PATCH 405/446] Use new env variable to get integration path --- openpype/hosts/unreal/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 0c39773c19..53a70c9b87 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -369,11 +369,11 @@ def get_compatible_integration( def get_path_to_cmdlet_project(ue_version: str) -> Path: cmd_project = Path( - os.path.abspath(os.getenv("OPENPYPE_ROOT"))) + os.path.abspath(os.getenv("AYON_UNREAL_ROOT"))) # For now, only tested on Windows (For Linux and Mac # it has to be implemented) - cmd_project /= f"openpype/hosts/unreal/integration/UE_{ue_version}" + cmd_project /= f"integration/UE_{ue_version}" # if the integration doesn't exist for current engine version # try to find the closest to it. From 7a5ecce6cc98bb4eefb25ccbd89e36aa193b0d61 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 2 Aug 2023 10:46:53 +0100 Subject: [PATCH 406/446] Better way to get integration path Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/unreal/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 53a70c9b87..6d544f65b2 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -369,7 +369,7 @@ def get_compatible_integration( def get_path_to_cmdlet_project(ue_version: str) -> Path: cmd_project = Path( - os.path.abspath(os.getenv("AYON_UNREAL_ROOT"))) + os.path.dirname(os.path.abspath(__file__))) # For now, only tested on Windows (For Linux and Mac # it has to be implemented) From ebd0d016b2ff03416441db7b28aef520b075796c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Aug 2023 13:02:34 +0200 Subject: [PATCH 407/446] updated unreal integration submodule --- openpype/hosts/unreal/integration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index ff15c70077..63266607ce 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit ff15c700771e719cc5f3d561ac5d6f7590623986 +Subproject commit 63266607ceb972a61484f046634ddfc9eb0b5757 From 31eabd4e6336606da8d3176421de447808adc553 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 2 Aug 2023 13:48:15 +0200 Subject: [PATCH 408/446] OP-4845 - sanitizing deadline url Deadline behaves weirdly if trailing slash is left in webservice url. This should remove it. --- .../plugins/publish/collect_deadline_server_from_instance.py | 2 ++ .../plugins/publish/collect_default_deadline_server.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py index 2de6073e29..eadfc3c83e 100644 --- a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py +++ b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py @@ -21,6 +21,8 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): def process(self, instance): instance.data["deadlineUrl"] = self._collect_deadline_url(instance) + instance.data["deadlineUrl"] = \ + instance.data["deadlineUrl"].strip().rstrip("/") self.log.info( "Using {} for submission.".format(instance.data["deadlineUrl"])) diff --git a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py index 1a0d615dc3..58721efad3 100644 --- a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -48,3 +48,6 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): context.data["defaultDeadline"] = deadline_webservice self.log.debug("Overriding from project settings with {}".format( # noqa: E501 deadline_webservice)) + + context.data["defaultDeadline"] = \ + context.data["defaultDeadline"].strip().rstrip("/") From b8e7ec291253c25f995539a171acb398c15b270b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 2 Aug 2023 14:08:22 +0200 Subject: [PATCH 409/446] Update openpype/modules/deadline/abstract_submit_deadline.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/modules/deadline/abstract_submit_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 9fcff111e6..3300bad6a9 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -399,7 +399,7 @@ class DeadlineJobInfo(object): def add_render_job_env_var(self): """Check if in OP or AYON mode and use appropriate env var.""" - if os.environ.get("USE_AYON_SERVER") == '1': + if AYON_SERVER_ENABLED: self.EnvironmentKeyValue["AYON_RENDER_JOB"] = "1" self.EnvironmentKeyValue["AYON_BUNDLE_NAME"] = ( os.environ["AYON_BUNDLE_NAME"]) From 67149111928fee1eae26bf93069a50c128e1cc29 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 2 Aug 2023 14:08:38 +0200 Subject: [PATCH 410/446] Update openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../modules/deadline/plugins/publish/submit_nuke_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 8f68a3a480..d427931c16 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -339,7 +339,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, # to recognize render jobs render_job_label = ( - "AYON_RENDER_JOB" if os.environ.get("USE_AYON_SERVER") == '1' + "AYON_RENDER_JOB" if AYON_SERVER_ENABLED else "OPENPYPE_RENDER_JOB") environment[render_job_label] = "1" From ee0a39d945a1b173432926231858ff1cfcecf6ef Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 2 Aug 2023 14:08:53 +0200 Subject: [PATCH 411/446] Update openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/submit_maya_remote_publish_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index d7440fd0f4..a9d4f7fbe8 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -118,7 +118,7 @@ class MayaSubmitRemotePublishDeadline( environment["OPENPYPE_PUBLISH_SUBSET"] = instance.data["subset"] environment["OPENPYPE_REMOTE_PUBLISH"] = "1" - if os.environ.get("USE_AYON_SERVER") == '1': + if AYON_SERVER_ENABLED: environment["AYON_REMOTE_PUBLISH"] = "1" else: environment["OPENPYPE_REMOTE_PUBLISH"] = "1" From 11c766eca742ac6c760a3bfcda1ffed397ee09d0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 2 Aug 2023 14:09:03 +0200 Subject: [PATCH 412/446] Update openpype/modules/deadline/plugins/publish/submit_publish_job.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index fc119a655a..0c25bda049 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -202,7 +202,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "IS_TEST": str(int(is_in_tests())) } - if os.environ.get("USE_AYON_SERVER") == '1': + if AYON_SERVER_ENABLED: environment["AYON_PUBLISH_JOB"] = "1" environment["AYON_RENDER_JOB"] = "0" environment["AYON_REMOTE_PUBLISH"] = "0" From 4b6bee1c76fde36befa659100df4cf1003576edf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 2 Aug 2023 14:13:56 +0200 Subject: [PATCH 413/446] OP-4845 - fix missing import AYON_SERVER_ENABLED --- openpype/modules/deadline/abstract_submit_deadline.py | 1 + .../plugins/publish/submit_maya_remote_publish_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_nuke_deadline.py | 2 ++ openpype/modules/deadline/plugins/publish/submit_publish_job.py | 1 + 4 files changed, 5 insertions(+) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 3300bad6a9..23e959d84c 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -25,6 +25,7 @@ from openpype.pipeline.publish import ( from openpype.pipeline.publish.lib import ( replace_with_published_scene_path ) +from openpype import AYON_SERVER_ENABLED JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index a9d4f7fbe8..988f8d106a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -4,6 +4,7 @@ from datetime import datetime from maya import cmds +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import legacy_io, PublishXmlValidationError from openpype.tests.lib import is_in_tests from openpype.lib import is_running_from_build diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index d427931c16..2bb7ca9662 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -8,6 +8,8 @@ import requests import pyblish.api import nuke + +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import legacy_io from openpype.pipeline.publish import ( OpenPypePyblishPluginMixin diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 0c25bda049..8d46f8241e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -15,6 +15,7 @@ from openpype.client import ( from openpype.pipeline import ( legacy_io, ) +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import publish from openpype.lib import EnumDef, is_running_from_build from openpype.tests.lib import is_in_tests From ed3e5a8c6b019cedbfb75bb875e545bc5643526f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 2 Aug 2023 14:20:07 +0200 Subject: [PATCH 414/446] OP-4845 - fix missing AYON_BUNDLE_NAME --- .../deadline/plugins/publish/submit_nuke_deadline.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 2bb7ca9662..93c6ad8139 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -340,9 +340,12 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, environment[_path] = os.environ[_path] # to recognize render jobs - render_job_label = ( - "AYON_RENDER_JOB" if AYON_SERVER_ENABLED - else "OPENPYPE_RENDER_JOB") + if AYON_SERVER_ENABLED: + environment["AYON_BUNDLE_NAME"] = os.environ["AYON_BUNDLE_NAME"] + render_job_label = "AYON_RENDER_JOB" + else: + render_job_label = "OPENPYPE_RENDER_JOB" + environment[render_job_label] = "1" # finally search replace in values of any key From c71aae5fd8088604028b3038bc64ac5772417e7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Aug 2023 14:21:24 +0200 Subject: [PATCH 415/446] added missing imports --- openpype/modules/deadline/abstract_submit_deadline.py | 1 + .../plugins/publish/submit_maya_remote_publish_deadline.py | 1 + .../deadline/plugins/publish/submit_nuke_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_publish_job.py | 6 ++---- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 3300bad6a9..c1a6eade46 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -11,6 +11,7 @@ import platform import getpass from functools import partial from collections import OrderedDict +from openpype import AYON_SERVER_ENABLED import six import attr diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index a9d4f7fbe8..988f8d106a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -4,6 +4,7 @@ from datetime import datetime from maya import cmds +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import legacy_io, PublishXmlValidationError from openpype.tests.lib import is_in_tests from openpype.lib import is_running_from_build diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index d427931c16..cafa71d3cb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -8,6 +8,7 @@ import requests import pyblish.api import nuke +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import legacy_io from openpype.pipeline.publish import ( OpenPypePyblishPluginMixin diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 0c25bda049..ec182fcd66 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -9,13 +9,11 @@ import clique import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_last_version_by_subset_name, ) -from openpype.pipeline import ( - legacy_io, -) -from openpype.pipeline import publish +from openpype.pipeline import publish, legacy_io from openpype.lib import EnumDef, is_running_from_build from openpype.tests.lib import is_in_tests From 997d8a7a30c7860fb90276ca636048950735bf6c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 2 Aug 2023 14:54:25 +0200 Subject: [PATCH 416/446] Update openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/submit_maya_remote_publish_deadline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 988f8d106a..0d23f44333 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -123,8 +123,6 @@ class MayaSubmitRemotePublishDeadline( environment["AYON_REMOTE_PUBLISH"] = "1" else: environment["OPENPYPE_REMOTE_PUBLISH"] = "1" - - for key, value in environment.items(): job_info.EnvironmentKeyValue[key] = value From ab1f0599d7bc59dec08c8f5092f281ee6a2d8a1a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 2 Aug 2023 14:54:34 +0200 Subject: [PATCH 417/446] Update openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../repository/custom/plugins/Ayon/Ayon.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py index ae7aa7df75..16149d7e20 100644 --- a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py +++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py @@ -131,18 +131,16 @@ class AyonDeadlinePlugin(DeadlinePlugin): frameRegex = Regex(pattern) while True: frameMatch = frameRegex.Match(arguments) - if frameMatch.Success: - paddingSize = int(frameMatch.Groups[1].Value) - if paddingSize > 0: - padding = StringUtils.ToZeroPaddedString(frame, - paddingSize, - False) - else: - padding = str(frame) - arguments = arguments.replace(frameMatch.Groups[0].Value, - padding) - else: + if not frameMatch.Success: break + paddingSize = int(frameMatch.Groups[1].Value) + if paddingSize > 0: + padding = StringUtils.ToZeroPaddedString( + frame, paddingSize, False) + else: + padding = str(frame) + arguments = arguments.replace( + frameMatch.Groups[0].Value, padding) return arguments From 1ddc9f2fd6aadbab6960c374427bb5fa570334fd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Aug 2023 15:46:26 +0200 Subject: [PATCH 418/446] nuke: split write node features --- openpype/hosts/nuke/api/plugin.py | 11 +++++++++++ .../hosts/nuke/plugins/create/create_write_image.py | 5 +---- .../nuke/plugins/create/create_write_prerender.py | 8 +------- .../hosts/nuke/plugins/create/create_write_render.py | 9 ++++----- .../schemas/template_nuke_write_attrs.json | 6 ++++++ 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index cfdb407d26..03dd8915d6 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -256,6 +256,17 @@ class NukeWriteCreator(NukeCreator): family = "write" icon = "sign-out" + def get_linked_knobs(self): + linked_knobs = [] + if "channels" in self.instance_attributes: + linked_knobs.append("channels") + if "ordered" in self.instance_attributes: + linked_knobs.append("render_order") + if "use_range_limit" in self.instance_attributes: + linked_knobs.extend(["___", "first", "last", "use_limit"]) + + return linked_knobs + def integrate_links(self, node, outputs=True): # skip if no selection if not self.selected_node: diff --git a/openpype/hosts/nuke/plugins/create/create_write_image.py b/openpype/hosts/nuke/plugins/create/create_write_image.py index 0c8adfb75c..8c18739587 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_image.py +++ b/openpype/hosts/nuke/plugins/create/create_write_image.py @@ -64,9 +64,6 @@ class CreateWriteImage(napi.NukeWriteCreator): ) def create_instance_node(self, subset_name, instance_data): - linked_knobs_ = [] - if "use_range_limit" in self.instance_attributes: - linked_knobs_ = ["channels", "___", "first", "last", "use_limit"] # add fpath_template write_data = { @@ -81,7 +78,7 @@ class CreateWriteImage(napi.NukeWriteCreator): write_data, input=self.selected_node, prenodes=self.prenodes, - linked_knobs=linked_knobs_, + linked_knobs=self.get_linked_knobs(), **{ "frame": nuke.frame() } diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index c3bba5f477..395c3b002f 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -45,12 +45,6 @@ class CreateWritePrerender(napi.NukeWriteCreator): return attr_defs def create_instance_node(self, subset_name, instance_data): - linked_knobs_ = [] - if "use_range_limit" in self.instance_attributes: - linked_knobs_ = ["channels", "___", "first", "last", "use_limit"] - - linked_knobs_.append("render_order") - # add fpath_template write_data = { "creator": self.__class__.__name__, @@ -73,7 +67,7 @@ class CreateWritePrerender(napi.NukeWriteCreator): write_data, input=self.selected_node, prenodes=self.prenodes, - linked_knobs=linked_knobs_, + linked_knobs=self.get_linked_knobs(), **{ "width": width, "height": height diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index aef4b06a2c..91acf4eabc 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -39,10 +39,6 @@ class CreateWriteRender(napi.NukeWriteCreator): return attr_defs def create_instance_node(self, subset_name, instance_data): - linked_knobs_ = [ - "channels", "___", "first", "last", "use_limit", "render_order" - ] - # add fpath_template write_data = { "creator": self.__class__.__name__, @@ -60,12 +56,15 @@ class CreateWriteRender(napi.NukeWriteCreator): actual_format = nuke.root().knob('format').value() width, height = (actual_format.width(), actual_format.height()) + self.log.debug(">>>>>>> : {}".format(self.instance_attributes)) + self.log.debug(">>>>>>> : {}".format(self.get_linked_knobs())) + created_node = napi.create_write_node( subset_name, write_data, input=self.selected_node, prenodes=self.prenodes, - linked_knobs=linked_knobs_, + linked_knobs=self.get_linked_knobs(), **{ "width": width, "height": height diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_write_attrs.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_write_attrs.json index 8be48e669d..3a34858f4e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_write_attrs.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_write_attrs.json @@ -13,6 +13,12 @@ }, { "use_range_limit": "Use range limit" + }, + { + "ordered": "Defined order" + }, + { + "channels": "Channels override" } ] } From dc8cd15f18ec39ae97e83e9381744a44970a5b34 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Aug 2023 17:00:47 +0200 Subject: [PATCH 419/446] nuke: subset name driven form node name also publisher variant change is reflected in node name --- openpype/hosts/nuke/api/pipeline.py | 22 ++++++++++++++++++++++ openpype/hosts/nuke/api/plugin.py | 13 ++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index fcc3becd2d..2871f8afbc 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -564,6 +564,9 @@ def list_instances(creator_id=None): if creator_id and instance_data["creator_identifier"] != creator_id: continue + # node name could change, so update subset name data + _update_subset_name_data(instance_data, node) + if "render_order" not in node.knobs(): subset_instances.append((node, instance_data)) continue @@ -589,6 +592,25 @@ def list_instances(creator_id=None): return ordered_instances +def _update_subset_name_data(instance_data, node): + """Update subset name data in instance data. + + Args: + instance_data (dict): instance creator data + node (nuke.Node): nuke node + """ + # make sure node name is subset name + old_subset_name = instance_data["subset"] + old_variant = instance_data["variant"] + subset_name_root = old_subset_name.replace(old_variant, "") + + new_subset_name = node.name() + new_variant = new_subset_name.replace(subset_name_root, "") + + instance_data["subset"] = new_subset_name + instance_data["variant"] = new_variant + + def remove_instance(instance): """Remove instance from current workfile metadata. diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index cfdb407d26..4a7bb03216 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -212,9 +212,20 @@ class NukeCreator(NewCreator): created_instance["creator_attributes"].pop(key) def update_instances(self, update_list): - for created_inst, _changes in update_list: + for created_inst, changes in update_list: instance_node = created_inst.transient_data["node"] + changed_keys = { + key: changes[key].new_value + for key in changes.changed_keys + } + + # update instance node name if subset name changed + if "subset" in changed_keys: + instance_node["name"].setValue( + changed_keys["subset"] + ) + # in case node is not existing anymore (user erased it manually) try: instance_node.fullName() From df78b060149dff6db27440aca8cee402be94cb0b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Aug 2023 17:32:50 +0200 Subject: [PATCH 420/446] Nuke: improve ordering publishing instances --- openpype/hosts/nuke/api/pipeline.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 2871f8afbc..045f7ec85d 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -539,6 +539,8 @@ def list_instances(creator_id=None): """ instances_by_order = defaultdict(list) subset_instances = [] + instance_ids = set() + for node in nuke.allNodes(recurseGroups=True): if node.Class() in ["Viewer", "Dot"]: @@ -564,6 +566,11 @@ def list_instances(creator_id=None): if creator_id and instance_data["creator_identifier"] != creator_id: continue + if instance_data["instance_id"] in instance_ids: + instance_data.pop("instance_id") + else: + instance_ids.add(instance_data["instance_id"]) + # node name could change, so update subset name data _update_subset_name_data(instance_data, node) @@ -575,19 +582,20 @@ def list_instances(creator_id=None): instances_by_order[order].append((node, instance_data)) # Sort instances based on order attribute or subset name. + # TODO: remove in future Publisher enhanced with sorting ordered_instances = [] for key in sorted(instances_by_order.keys()): - instances_by_subset = {} - for node, data in instances_by_order[key]: - instances_by_subset[data["subset"]] = (node, data) + instances_by_subset = defaultdict(list) + for node, data_ in instances_by_order[key]: + instances_by_subset[data_["subset"]].append((node, data_)) for subkey in sorted(instances_by_subset.keys()): - ordered_instances.append(instances_by_subset[subkey]) + ordered_instances.extend(instances_by_subset[subkey]) - instances_by_subset = {} - for node, data in subset_instances: - instances_by_subset[data["subset"]] = (node, data) + instances_by_subset = defaultdict(list) + for node, data_ in subset_instances: + instances_by_subset[data_["subset"]].append((node, data_)) for key in sorted(instances_by_subset.keys()): - ordered_instances.append(instances_by_subset[key]) + ordered_instances.extend(instances_by_subset[key]) return ordered_instances From ba7dca9a255e1b23b28d1c1e207b74ee19a1c789 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Aug 2023 18:24:59 +0200 Subject: [PATCH 421/446] Publisher: Fix create/publish animation (#5369) * use geometry movement instead of min/max width * take height in calculation too * right parenting of widgets --- .../publisher/widgets/overview_widget.py | 127 ++++++++++-------- openpype/tools/publisher/window.py | 26 ++-- 2 files changed, 88 insertions(+), 65 deletions(-) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 25fff73134..470645b9ee 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -28,12 +28,14 @@ class OverviewWidget(QtWidgets.QFrame): self._refreshing_instances = False self._controller = controller - create_widget = CreateWidget(controller, self) + subset_content_widget = QtWidgets.QWidget(self) + + create_widget = CreateWidget(controller, subset_content_widget) # --- Created Subsets/Instances --- # Common widget for creation and overview subset_views_widget = BorderedLabelWidget( - "Subsets to publish", self + "Subsets to publish", subset_content_widget ) subset_view_cards = InstanceCardView(controller, subset_views_widget) @@ -45,14 +47,14 @@ class OverviewWidget(QtWidgets.QFrame): subset_views_layout.setCurrentWidget(subset_view_cards) # Buttons at the bottom of subset view - create_btn = CreateInstanceBtn(self) - delete_btn = RemoveInstanceBtn(self) - change_view_btn = ChangeViewBtn(self) + create_btn = CreateInstanceBtn(subset_views_widget) + delete_btn = RemoveInstanceBtn(subset_views_widget) + change_view_btn = ChangeViewBtn(subset_views_widget) # --- Overview --- # Subset details widget subset_attributes_wrap = BorderedLabelWidget( - "Publish options", self + "Publish options", subset_content_widget ) subset_attributes_widget = SubsetAttributesWidget( controller, subset_attributes_wrap @@ -81,7 +83,6 @@ class OverviewWidget(QtWidgets.QFrame): subset_views_widget.set_center_widget(subset_view_widget) # Whole subset layout with attributes and details - subset_content_widget = QtWidgets.QWidget(self) subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget) subset_content_layout.setContentsMargins(0, 0, 0, 0) subset_content_layout.addWidget(create_widget, 7) @@ -161,44 +162,62 @@ class OverviewWidget(QtWidgets.QFrame): self._change_anim = change_anim # Start in create mode - self._create_widget_policy = create_widget.sizePolicy() - self._subset_views_widget_policy = subset_views_widget.sizePolicy() - self._subset_attributes_wrap_policy = ( - subset_attributes_wrap.sizePolicy() - ) - self._max_widget_width = None self._current_state = "create" subset_attributes_wrap.setVisible(False) + def make_sure_animation_is_finished(self): + if self._change_anim.state() == QtCore.QAbstractAnimation.Running: + self._change_anim.stop() + self._on_change_anim_finished() + def set_state(self, new_state, animate): if new_state == self._current_state: return self._current_state = new_state - anim_is_running = ( - self._change_anim.state() == QtCore.QAbstractAnimation.Running - ) if not animate: - self._change_visibility_for_state() - if anim_is_running: - self._change_anim.stop() + self.make_sure_animation_is_finished() return - if self._max_widget_width is None: - self._max_widget_width = self._subset_views_widget.maximumWidth() - if new_state == "create": direction = QtCore.QAbstractAnimation.Backward else: direction = QtCore.QAbstractAnimation.Forward self._change_anim.setDirection(direction) - if not anim_is_running: - view_width = self._subset_views_widget.width() - self._subset_views_widget.setMinimumWidth(view_width) - self._subset_views_widget.setMaximumWidth(view_width) + if ( + self._change_anim.state() != QtCore.QAbstractAnimation.Running + ): + self._start_animation() + + def _start_animation(self): + views_geo = self._subset_views_widget.geometry() + layout_spacing = self._subset_content_layout.spacing() + if self._create_widget.isVisible(): + create_geo = self._create_widget.geometry() + subset_geo = QtCore.QRect(create_geo) + subset_geo.moveTop(views_geo.top()) + subset_geo.moveLeft(views_geo.right() + layout_spacing) + self._subset_attributes_wrap.setVisible(True) + + elif self._subset_attributes_wrap.isVisible(): + subset_geo = self._subset_attributes_wrap.geometry() + create_geo = QtCore.QRect(subset_geo) + create_geo.moveTop(views_geo.top()) + create_geo.moveRight(views_geo.left() - (layout_spacing + 1)) + self._create_widget.setVisible(True) + else: self._change_anim.start() + return + + while self._subset_content_layout.count(): + self._subset_content_layout.takeAt(0) + self._subset_views_widget.setGeometry(views_geo) + self._create_widget.setGeometry(create_geo) + self._subset_attributes_wrap.setGeometry(subset_geo) + + self._change_anim.start() def get_subset_views_geo(self): parent = self._subset_views_widget.parent() @@ -281,41 +300,39 @@ class OverviewWidget(QtWidgets.QFrame): def _on_change_anim(self, value): self._create_widget.setVisible(True) self._subset_attributes_wrap.setVisible(True) - width = ( - self._subset_content_widget.width() - - ( - self._subset_views_widget.width() - + (self._subset_content_layout.spacing() * 2) - ) - ) - subset_attrs_width = int((float(width) / self.anim_end_value) * value) - if subset_attrs_width > width: - subset_attrs_width = width + layout_spacing = self._subset_content_layout.spacing() + content_width = ( + self._subset_content_widget.width() - (layout_spacing * 2) + ) + content_height = self._subset_content_widget.height() + views_width = max( + int(content_width * 0.3), + self._subset_views_widget.minimumWidth() + ) + width = content_width - views_width + # Visible widths of other widgets + subset_attrs_width = int((float(width) / self.anim_end_value) * value) create_width = width - subset_attrs_width - self._create_widget.setMinimumWidth(create_width) - self._create_widget.setMaximumWidth(create_width) - self._subset_attributes_wrap.setMinimumWidth(subset_attrs_width) - self._subset_attributes_wrap.setMaximumWidth(subset_attrs_width) + views_geo = QtCore.QRect( + create_width + layout_spacing, 0, + views_width, content_height + ) + create_geo = QtCore.QRect(0, 0, width, content_height) + subset_attrs_geo = QtCore.QRect(create_geo) + create_geo.moveRight(views_geo.left() - (layout_spacing + 1)) + subset_attrs_geo.moveLeft(views_geo.right() + layout_spacing) + + self._subset_views_widget.setGeometry(views_geo) + self._create_widget.setGeometry(create_geo) + self._subset_attributes_wrap.setGeometry(subset_attrs_geo) def _on_change_anim_finished(self): self._change_visibility_for_state() - self._create_widget.setMinimumWidth(0) - self._create_widget.setMaximumWidth(self._max_widget_width) - self._subset_attributes_wrap.setMinimumWidth(0) - self._subset_attributes_wrap.setMaximumWidth(self._max_widget_width) - self._subset_views_widget.setMinimumWidth(0) - self._subset_views_widget.setMaximumWidth(self._max_widget_width) - self._create_widget.setSizePolicy( - self._create_widget_policy - ) - self._subset_attributes_wrap.setSizePolicy( - self._subset_attributes_wrap_policy - ) - self._subset_views_widget.setSizePolicy( - self._subset_views_widget_policy - ) + self._subset_content_layout.addWidget(self._create_widget, 7) + self._subset_content_layout.addWidget(self._subset_views_widget, 3) + self._subset_content_layout.addWidget(self._subset_attributes_wrap, 7) def _change_visibility_for_state(self): self._create_widget.setVisible( diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 2bda0c1cfe..39e78c01bb 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -634,16 +634,7 @@ class PublisherWindow(QtWidgets.QDialog): if old_tab == "details": self._publish_details_widget.close_details_popup() - if new_tab in ("create", "publish"): - animate = True - if old_tab not in ("create", "publish"): - animate = False - self._content_stacked_layout.setCurrentWidget( - self._overview_widget - ) - self._overview_widget.set_state(new_tab, animate) - - elif new_tab == "details": + if new_tab == "details": self._content_stacked_layout.setCurrentWidget( self._publish_details_widget ) @@ -654,6 +645,21 @@ class PublisherWindow(QtWidgets.QDialog): self._report_widget ) + old_on_overview = old_tab in ("create", "publish") + if new_tab in ("create", "publish"): + self._content_stacked_layout.setCurrentWidget( + self._overview_widget + ) + # Overview state is animated only when switching between + # 'create' and 'publish' tab + self._overview_widget.set_state(new_tab, old_on_overview) + + elif old_on_overview: + # Make sure animation finished if previous tab was 'create' + # or 'publish'. That is just for safety to avoid stuck animation + # when user clicks too fast. + self._overview_widget.make_sure_animation_is_finished() + is_create = new_tab == "create" if is_create: self._install_app_event_listener() From 0c423a9a32e831d7ea196bfa199d99e55b6c2bfe Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 2 Aug 2023 20:55:54 +0300 Subject: [PATCH 422/446] delete redundant bgeo sop validator --- .../publish/validate_bgeo_file_sop_path.py | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py diff --git a/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py b/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py deleted file mode 100644 index 22746aabb0..0000000000 --- a/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validator plugin for SOP Path in bgeo isntance.""" -import pyblish.api -from openpype.pipeline import PublishValidationError - - -class ValidateNoSOPPath(pyblish.api.InstancePlugin): - """Validate if SOP Path in BGEO instance exists.""" - - order = pyblish.api.ValidatorOrder - families = ["bgeo"] - label = "Validate BGEO SOP Path" - - def process(self, instance): - - import hou - - node = hou.node(instance.data.get("instance_node")) - sop_path = node.evalParm("soppath") - if not sop_path: - raise PublishValidationError( - ("Empty SOP Path ('soppath' parameter) found in " - f"the BGEO instance Geometry - {node.path()}")) - if not isinstance(hou.node(sop_path), hou.SopNode): - raise PublishValidationError( - "SOP path is not pointing to valid SOP node.") From 7e9f42b4479dca34fa21bcdb73950da85f757542 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Aug 2023 10:04:15 +0200 Subject: [PATCH 423/446] Applications: Use prelaunch hooks to extract environments (#5387) * ApplicationManager can have more granular way how applications are launched * executable is optional to be able create ApplicationLaunchContext * launch context can run prelaunch hooks without launching application * 'get_app_environments_for_context' is using launch context to prepare environments * added 'launch_type' as one of filtering options for LaunchHook * added 'local' launch type filter to existing launch hooks * define 'automated' launch type in remote publish function * modified publish and extract environments cli commands * launch types are only for local by default * fix import * fix launch types of global host data * change order or kwargs * change unreal filter attribute --- openpype/hooks/pre_add_last_workfile_arg.py | 3 +- openpype/hooks/pre_copy_template_workfile.py | 3 +- .../hooks/pre_create_extra_workdir_folders.py | 3 +- openpype/hooks/pre_foundry_apps.py | 3 +- openpype/hooks/pre_global_host_data.py | 3 +- openpype/hooks/pre_mac_launch.py | 3 +- openpype/hooks/pre_non_python_host_launch.py | 9 +- openpype/hooks/pre_ocio_hook.py | 1 + .../hooks/pre_add_run_python_script_arg.py | 7 +- .../hosts/blender/hooks/pre_pyside_install.py | 5 +- .../blender/hooks/pre_windows_console.py | 3 +- .../celaction/hooks/pre_celaction_setup.py | 4 +- openpype/hosts/flame/hooks/pre_flame_setup.py | 6 +- .../fusion/hooks/pre_fusion_profile_hook.py | 7 +- .../hosts/fusion/hooks/pre_fusion_setup.py | 7 +- openpype/hosts/houdini/hooks/set_paths.py | 3 +- .../hosts/max/hooks/force_startup_script.py | 3 +- openpype/hosts/max/hooks/inject_python.py | 3 +- openpype/hosts/max/hooks/set_paths.py | 3 +- .../hosts/maya/hooks/pre_auto_load_plugins.py | 3 +- openpype/hosts/maya/hooks/pre_copy_mel.py | 3 +- .../pre_open_workfile_post_initialization.py | 3 +- .../hosts/nuke/hooks/pre_nukeassist_setup.py | 3 +- .../hooks/pre_resolve_last_workfile.py | 3 +- .../hosts/resolve/hooks/pre_resolve_setup.py | 3 +- .../resolve/hooks/pre_resolve_startup.py | 3 +- .../hosts/tvpaint/hooks/pre_launch_args.py | 7 +- .../unreal/hooks/pre_workfile_preparation.py | 5 +- .../hosts/webpublisher/publish_functions.py | 45 ++-- openpype/lib/applications.py | 223 ++++++++++++------ .../launch_hooks/post_ftrack_changes.py | 3 +- .../slack/launch_hooks/pre_python2_vendor.py | 3 +- .../pre_copy_last_published_workfile.py | 9 +- .../launch_hooks/post_start_timer.py | 3 +- openpype/pype_commands.py | 20 +- 35 files changed, 266 insertions(+), 152 deletions(-) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index c54acbc203..0e43f1bfe6 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -1,6 +1,6 @@ import os -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class AddLastWorkfileToLaunchArgs(PreLaunchHook): @@ -28,6 +28,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "substancepainter", "aftereffects" ] + launch_types = {LaunchTypes.local} def execute(self): if not self.data.get("start_last_workfile"): diff --git a/openpype/hooks/pre_copy_template_workfile.py b/openpype/hooks/pre_copy_template_workfile.py index 70c549919f..9962dabdd8 100644 --- a/openpype/hooks/pre_copy_template_workfile.py +++ b/openpype/hooks/pre_copy_template_workfile.py @@ -1,7 +1,7 @@ import os import shutil -from openpype.lib import PreLaunchHook from openpype.settings import get_project_settings +from openpype.lib.applications import PreLaunchHook, LaunchTypes from openpype.pipeline.workfile import ( get_custom_workfile_template, get_custom_workfile_template_by_string_context @@ -20,6 +20,7 @@ class CopyTemplateWorkfile(PreLaunchHook): # Before `AddLastWorkfileToLaunchArgs` order = 0 app_groups = ["blender", "photoshop", "tvpaint", "aftereffects"] + launch_types = {LaunchTypes.local} def execute(self): """Check if can copy template for context and do it if possible. diff --git a/openpype/hooks/pre_create_extra_workdir_folders.py b/openpype/hooks/pre_create_extra_workdir_folders.py index 8856281120..4c9d08b375 100644 --- a/openpype/hooks/pre_create_extra_workdir_folders.py +++ b/openpype/hooks/pre_create_extra_workdir_folders.py @@ -1,5 +1,5 @@ import os -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes from openpype.pipeline.workfile import create_workdir_extra_folders @@ -14,6 +14,7 @@ class CreateWorkdirExtraFolders(PreLaunchHook): # Execute after workfile template copy order = 15 + launch_types = {LaunchTypes.local} def execute(self): if not self.application.is_host: diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 21ec8e7881..50e50e74a2 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -1,5 +1,5 @@ import subprocess -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class LaunchFoundryAppsWindows(PreLaunchHook): @@ -15,6 +15,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook): order = 1000 app_groups = ["nuke", "nukeassist", "nukex", "hiero", "nukestudio"] platforms = ["windows"] + launch_types = {LaunchTypes.local} def execute(self): # Change `creationflags` to CREATE_NEW_CONSOLE diff --git a/openpype/hooks/pre_global_host_data.py b/openpype/hooks/pre_global_host_data.py index 260e28a18b..813df24af0 100644 --- a/openpype/hooks/pre_global_host_data.py +++ b/openpype/hooks/pre_global_host_data.py @@ -1,5 +1,5 @@ from openpype.client import get_project, get_asset_by_name -from openpype.lib import ( +from openpype.lib.applications import ( PreLaunchHook, EnvironmentPrepData, prepare_app_environments, @@ -10,6 +10,7 @@ from openpype.pipeline import Anatomy class GlobalHostDataHook(PreLaunchHook): order = -100 + launch_types = set() def execute(self): """Prepare global objects to `data` that will be used for sure.""" diff --git a/openpype/hooks/pre_mac_launch.py b/openpype/hooks/pre_mac_launch.py index f85557a4f0..298346c9b1 100644 --- a/openpype/hooks/pre_mac_launch.py +++ b/openpype/hooks/pre_mac_launch.py @@ -1,5 +1,5 @@ import os -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class LaunchWithTerminal(PreLaunchHook): @@ -13,6 +13,7 @@ class LaunchWithTerminal(PreLaunchHook): order = 1000 platforms = ["darwin"] + launch_types = {LaunchTypes.local} def execute(self): executable = str(self.launch_context.executable) diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py index 043cb3c7f6..e58c354360 100644 --- a/openpype/hooks/pre_non_python_host_launch.py +++ b/openpype/hooks/pre_non_python_host_launch.py @@ -1,10 +1,11 @@ import os -from openpype.lib import ( +from openpype.lib import get_openpype_execute_args +from openpype.lib.applications import ( + get_non_python_host_kwargs, PreLaunchHook, - get_openpype_execute_args + LaunchTypes, ) -from openpype.lib.applications import get_non_python_host_kwargs from openpype import PACKAGE_DIR as OPENPYPE_DIR @@ -19,6 +20,7 @@ class NonPythonHostHook(PreLaunchHook): app_groups = ["harmony", "photoshop", "aftereffects"] order = 20 + launch_types = {LaunchTypes.local} def execute(self): # Pop executable @@ -54,4 +56,3 @@ class NonPythonHostHook(PreLaunchHook): self.launch_context.kwargs = \ get_non_python_host_kwargs(self.launch_context.kwargs) - diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index 8f462665bc..7c53d3db66 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -22,6 +22,7 @@ class OCIOEnvHook(PreLaunchHook): "hiero", "resolve" ] + launch_types = set() def execute(self): """Hook entry method.""" diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 559e9ae0ce..68c9bfdd57 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -1,6 +1,6 @@ from pathlib import Path -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class AddPythonScriptToLaunchArgs(PreLaunchHook): @@ -8,9 +8,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): # Append after file argument order = 15 - app_groups = [ - "blender", - ] + app_groups = {"blender"} + launch_types = {LaunchTypes.local} def execute(self): if not self.launch_context.data.get("python_scripts"): diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index e5f66d2a26..777e383215 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -2,7 +2,7 @@ import os import re import subprocess from platform import system -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class InstallPySideToBlender(PreLaunchHook): @@ -16,7 +16,8 @@ class InstallPySideToBlender(PreLaunchHook): blender's python packages. """ - app_groups = ["blender"] + app_groups = {"blender"} + launch_types = {LaunchTypes.local} def execute(self): # Prelaunch hook is not crucial diff --git a/openpype/hosts/blender/hooks/pre_windows_console.py b/openpype/hosts/blender/hooks/pre_windows_console.py index d6be45b225..c6ecf284ef 100644 --- a/openpype/hosts/blender/hooks/pre_windows_console.py +++ b/openpype/hosts/blender/hooks/pre_windows_console.py @@ -1,5 +1,5 @@ import subprocess -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class BlenderConsoleWindows(PreLaunchHook): @@ -15,6 +15,7 @@ class BlenderConsoleWindows(PreLaunchHook): order = 1000 app_groups = ["blender"] platforms = ["windows"] + launch_types = {LaunchTypes.local} def execute(self): # Change `creationflags` to CREATE_NEW_CONSOLE diff --git a/openpype/hosts/celaction/hooks/pre_celaction_setup.py b/openpype/hosts/celaction/hooks/pre_celaction_setup.py index 96e784875c..df27195e60 100644 --- a/openpype/hosts/celaction/hooks/pre_celaction_setup.py +++ b/openpype/hosts/celaction/hooks/pre_celaction_setup.py @@ -2,7 +2,8 @@ import os import shutil import winreg import subprocess -from openpype.lib import PreLaunchHook, get_openpype_execute_args +from openpype.lib import get_openpype_execute_args +from openpype.lib.applications import PreLaunchHook, LaunchTypes from openpype.hosts.celaction import scripts CELACTION_SCRIPTS_DIR = os.path.dirname( @@ -16,6 +17,7 @@ class CelactionPrelaunchHook(PreLaunchHook): """ app_groups = ["celaction"] platforms = ["windows"] + launch_types = {LaunchTypes.local} def execute(self): asset_doc = self.data["asset_doc"] diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 83110bb6b5..61e3200d89 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -6,13 +6,10 @@ import socket from pprint import pformat from openpype.lib import ( - PreLaunchHook, get_openpype_username, run_subprocess, ) -from openpype.lib.applications import ( - ApplicationLaunchFailed -) +from openpype.lib.applications import PreLaunchHook, LaunchTypes from openpype.hosts import flame as opflame @@ -27,6 +24,7 @@ class FlamePrelaunch(PreLaunchHook): wtc_script_path = os.path.join( opflame.HOST_DIR, "api", "scripts", "wiretap_com.py") + launch_types = {LaunchTypes.local} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py b/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py index fd726ccda1..da74f8e1fe 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py @@ -2,12 +2,16 @@ import os import shutil import platform from pathlib import Path -from openpype.lib import PreLaunchHook, ApplicationLaunchFailed from openpype.hosts.fusion import ( FUSION_HOST_DIR, FUSION_VERSIONS_DICT, get_fusion_version, ) +from openpype.lib.applications import ( + PreLaunchHook, + LaunchTypes, + ApplicationLaunchFailed, +) class FusionCopyPrefsPrelaunch(PreLaunchHook): @@ -23,6 +27,7 @@ class FusionCopyPrefsPrelaunch(PreLaunchHook): app_groups = ["fusion"] order = 2 + launch_types = {LaunchTypes.local} def get_fusion_profile_name(self, profile_version) -> str: # Returns 'Default', unless FUSION16_PROFILE is set diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index f27cd1674b..68ef23d520 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -1,5 +1,9 @@ import os -from openpype.lib import PreLaunchHook, ApplicationLaunchFailed +from openpype.lib.applications import ( + PreLaunchHook, + LaunchTypes, + ApplicationLaunchFailed, +) from openpype.hosts.fusion import ( FUSION_HOST_DIR, FUSION_VERSIONS_DICT, @@ -19,6 +23,7 @@ class FusionPrelaunch(PreLaunchHook): app_groups = ["fusion"] order = 1 + launch_types = {LaunchTypes.local} def execute(self): # making sure python 3 is installed at provided path diff --git a/openpype/hosts/houdini/hooks/set_paths.py b/openpype/hosts/houdini/hooks/set_paths.py index 04a33b1643..2e7bf51757 100644 --- a/openpype/hosts/houdini/hooks/set_paths.py +++ b/openpype/hosts/houdini/hooks/set_paths.py @@ -1,4 +1,4 @@ -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class SetPath(PreLaunchHook): @@ -7,6 +7,7 @@ class SetPath(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ app_groups = ["houdini"] + launch_types = {LaunchTypes.local} def execute(self): workdir = self.launch_context.env.get("AVALON_WORKDIR", "") diff --git a/openpype/hosts/max/hooks/force_startup_script.py b/openpype/hosts/max/hooks/force_startup_script.py index 4fcf4fef21..701e348293 100644 --- a/openpype/hosts/max/hooks/force_startup_script.py +++ b/openpype/hosts/max/hooks/force_startup_script.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Pre-launch to force 3ds max startup script.""" -from openpype.lib import PreLaunchHook import os +from openpype.lib.applications import PreLaunchHook, LaunchTypes class ForceStartupScript(PreLaunchHook): @@ -15,6 +15,7 @@ class ForceStartupScript(PreLaunchHook): """ app_groups = ["3dsmax"] order = 11 + launch_types = {LaunchTypes.local} def execute(self): startup_args = [ diff --git a/openpype/hosts/max/hooks/inject_python.py b/openpype/hosts/max/hooks/inject_python.py index d9753ccbd8..bbfc95c078 100644 --- a/openpype/hosts/max/hooks/inject_python.py +++ b/openpype/hosts/max/hooks/inject_python.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Pre-launch hook to inject python environment.""" -from openpype.lib import PreLaunchHook import os +from openpype.lib.applications import PreLaunchHook, LaunchTypes class InjectPythonPath(PreLaunchHook): @@ -14,6 +14,7 @@ class InjectPythonPath(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ app_groups = ["3dsmax"] + launch_types = {LaunchTypes.local} def execute(self): self.launch_context.env["MAX_PYTHONPATH"] = os.environ["PYTHONPATH"] diff --git a/openpype/hosts/max/hooks/set_paths.py b/openpype/hosts/max/hooks/set_paths.py index 3db5306344..f06efff7c8 100644 --- a/openpype/hosts/max/hooks/set_paths.py +++ b/openpype/hosts/max/hooks/set_paths.py @@ -1,4 +1,4 @@ -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class SetPath(PreLaunchHook): @@ -7,6 +7,7 @@ class SetPath(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ app_groups = ["max"] + launch_types = {LaunchTypes.local} def execute(self): workdir = self.launch_context.env.get("AVALON_WORKDIR", "") diff --git a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py index 689d7adb4f..0437b6fd9d 100644 --- a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py +++ b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py @@ -1,4 +1,4 @@ -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class MayaPreAutoLoadPlugins(PreLaunchHook): @@ -7,6 +7,7 @@ class MayaPreAutoLoadPlugins(PreLaunchHook): # Before AddLastWorkfileToLaunchArgs order = 9 app_groups = ["maya"] + launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/maya/hooks/pre_copy_mel.py b/openpype/hosts/maya/hooks/pre_copy_mel.py index 9cea829ad7..ebb0c521c9 100644 --- a/openpype/hosts/maya/hooks/pre_copy_mel.py +++ b/openpype/hosts/maya/hooks/pre_copy_mel.py @@ -1,4 +1,4 @@ -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes from openpype.hosts.maya.lib import create_workspace_mel @@ -8,6 +8,7 @@ class PreCopyMel(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ app_groups = ["maya"] + launch_types = {LaunchTypes.local} def execute(self): project_doc = self.data["project_doc"] diff --git a/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py index 7582ce0591..0c1fd0efe3 100644 --- a/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py +++ b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py @@ -1,4 +1,4 @@ -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class MayaPreOpenWorkfilePostInitialization(PreLaunchHook): @@ -7,6 +7,7 @@ class MayaPreOpenWorkfilePostInitialization(PreLaunchHook): # Before AddLastWorkfileToLaunchArgs. order = 9 app_groups = ["maya"] + launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py index 3948a665c6..bdb271e3f1 100644 --- a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py +++ b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py @@ -1,4 +1,4 @@ -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook class PrelaunchNukeAssistHook(PreLaunchHook): @@ -6,6 +6,7 @@ class PrelaunchNukeAssistHook(PreLaunchHook): Adding flag when nukeassist """ app_groups = ["nukeassist"] + launch_types = set() def execute(self): self.launch_context.env["NUKEASSIST"] = "1" diff --git a/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py index bc03baad8d..dc986ec1d2 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py @@ -1,5 +1,5 @@ import os -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes class PreLaunchResolveLastWorkfile(PreLaunchHook): @@ -10,6 +10,7 @@ class PreLaunchResolveLastWorkfile(PreLaunchHook): """ order = 10 app_groups = ["resolve"] + launch_types = {LaunchTypes.local} def execute(self): if not self.data.get("start_last_workfile"): diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 3fd39d665c..389256f4da 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -1,7 +1,7 @@ import os from pathlib import Path import platform -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes from openpype.hosts.resolve.utils import setup @@ -31,6 +31,7 @@ class PreLaunchResolveSetup(PreLaunchHook): """ app_groups = ["resolve"] + launch_types = {LaunchTypes.local} def execute(self): current_platform = platform.system().lower() diff --git a/openpype/hosts/resolve/hooks/pre_resolve_startup.py b/openpype/hosts/resolve/hooks/pre_resolve_startup.py index 599e0c0008..649af817ae 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_startup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_startup.py @@ -1,6 +1,6 @@ import os -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook, LaunchTypes import openpype.hosts.resolve @@ -10,6 +10,7 @@ class PreLaunchResolveStartup(PreLaunchHook): """ order = 11 app_groups = ["resolve"] + launch_types = {LaunchTypes.local} def execute(self): # Set the openpype prelaunch startup script path for easy access diff --git a/openpype/hosts/tvpaint/hooks/pre_launch_args.py b/openpype/hosts/tvpaint/hooks/pre_launch_args.py index c31403437a..065da316ab 100644 --- a/openpype/hosts/tvpaint/hooks/pre_launch_args.py +++ b/openpype/hosts/tvpaint/hooks/pre_launch_args.py @@ -1,7 +1,5 @@ -from openpype.lib import ( - PreLaunchHook, - get_openpype_execute_args -) +from openpype.lib import get_openpype_execute_args +from openpype.lib.applications import PreLaunchHook, LaunchTypes class TvpaintPrelaunchHook(PreLaunchHook): @@ -14,6 +12,7 @@ class TvpaintPrelaunchHook(PreLaunchHook): to copy templated workfile from predefined path. """ app_groups = ["tvpaint"] + launch_types = {LaunchTypes.local} def execute(self): # Pop tvpaint executable diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index e5010366b8..202d7854f6 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -7,9 +7,10 @@ from pathlib import Path from qtpy import QtCore from openpype import resources -from openpype.lib import ( +from openpype.lib.applications import ( PreLaunchHook, ApplicationLaunchFailed, + LaunchTypes, ) from openpype.pipeline.workfile import get_workfile_template_key import openpype.hosts.unreal.lib as unreal_lib @@ -29,6 +30,8 @@ class UnrealPrelaunchHook(PreLaunchHook): shell script. """ + app_groups = {"unreal"} + launch_types = {LaunchTypes.local} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/openpype/hosts/webpublisher/publish_functions.py b/openpype/hosts/webpublisher/publish_functions.py index 83f53ced68..41aab68cce 100644 --- a/openpype/hosts/webpublisher/publish_functions.py +++ b/openpype/hosts/webpublisher/publish_functions.py @@ -6,7 +6,7 @@ import pyblish.util from openpype.lib import Logger from openpype.lib.applications import ( ApplicationManager, - get_app_environments_for_context, + LaunchTypes, ) from openpype.pipeline import install_host from openpype.hosts.webpublisher.api import WebpublisherHost @@ -156,22 +156,31 @@ def cli_publish_from_app( found_variant_key = find_variant_key(application_manager, host_name) app_name = "{}/{}".format(host_name, found_variant_key) + data = { + "last_workfile_path": workfile_path, + "start_last_workfile": True, + "project_name": project_name, + "asset_name": asset_name, + "task_name": task_name, + "launch_type": LaunchTypes.automated, + } + launch_context = application_manager.create_launch_context( + app_name, **data) + launch_context.run_prelaunch_hooks() + # must have for proper launch of app - env = get_app_environments_for_context( - project_name, - asset_name, - task_name, - app_name - ) + env = launch_context.env print("env:: {}".format(env)) + env["OPENPYPE_PUBLISH_DATA"] = batch_path + # must pass identifier to update log lines for a batch + env["BATCH_LOG_ID"] = str(_id) + env["HEADLESS_PUBLISH"] = 'true' # to use in app lib + env["USER_EMAIL"] = user_email + os.environ.update(env) - os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path - # must pass identifier to update log lines for a batch - os.environ["BATCH_LOG_ID"] = str(_id) - os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib - os.environ["USER_EMAIL"] = user_email - + # Why is this here? Registered host in this process does not affect + # regitered host in launched process. pyblish.api.register_host(host_name) if targets: if isinstance(targets, str): @@ -184,15 +193,7 @@ def cli_publish_from_app( os.environ["PYBLISH_TARGETS"] = os.pathsep.join( set(current_targets)) - data = { - "last_workfile_path": workfile_path, - "start_last_workfile": True, - "project_name": project_name, - "asset_name": asset_name, - "task_name": task_name - } - - launched_app = application_manager.launch(app_name, **data) + launched_app = application_manager.launch_with_context(launch_context) timeout = get_timeout(project_name, host_name, task_type) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index fac3e33f71..ff5e27c122 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -12,10 +12,6 @@ from abc import ABCMeta, abstractmethod import six from openpype import AYON_SERVER_ENABLED, PACKAGE_DIR -from openpype.client import ( - get_project, - get_asset_by_name, -) from openpype.settings import ( get_system_settings, get_project_settings, @@ -47,6 +43,25 @@ CUSTOM_LAUNCH_APP_GROUPS = { } +class LaunchTypes: + """Launch types are filters for pre/post-launch hooks. + + Please use these variables in case they'll change values. + """ + + # Local launch - application is launched on local machine + local = "local" + # Farm render job - application is on farm + farm_render = "farm-render" + # Farm publish job - integration post-render job + farm_publish = "farm-publish" + # Remote launch - application is launched on remote machine from which + # can be started publishing + remote = "remote" + # Automated launch - application is launched with automated publishing + automated = "automated" + + def parse_environments(env_data, env_group=None, platform_name=None): """Parse environment values from settings byt group and platform. @@ -483,6 +498,42 @@ class ApplicationManager: break return output + def create_launch_context(self, app_name, **data): + """Prepare launch context for application. + + Args: + app_name (str): Name of application that should be launched. + **data (Any): Any additional data. Data may be used during + + Returns: + ApplicationLaunchContext: Launch context for application. + + Raises: + ApplicationNotFound: Application was not found by entered name. + """ + + app = self.applications.get(app_name) + if not app: + raise ApplicationNotFound(app_name) + + executable = app.find_executable() + + return ApplicationLaunchContext( + app, executable, **data + ) + + def launch_with_context(self, launch_context): + """Launch application using existing launch context. + + Args: + launch_context (ApplicationLaunchContext): Prepared launch + context. + """ + + if not launch_context.executable: + raise ApplictionExecutableNotFound(launch_context.application) + return launch_context.launch() + def launch(self, app_name, **data): """Launch procedure. @@ -503,18 +554,10 @@ class ApplicationManager: failed. Exception should contain explanation message, traceback should not be needed. """ - app = self.applications.get(app_name) - if not app: - raise ApplicationNotFound(app_name) - executable = app.find_executable() - if not executable: - raise ApplictionExecutableNotFound(app) + context = self.create_launch_context(app_name, **data) + return self.launch_with_context(context) - context = ApplicationLaunchContext( - app, executable, **data - ) - return context.launch() class EnvironmentToolGroup: @@ -736,13 +779,17 @@ class LaunchHook: # Order of prelaunch hook, will be executed as last if set to None. order = None # List of host implementations, skipped if empty. - hosts = [] - # List of application groups - app_groups = [] - # List of specific application names - app_names = [] - # List of platform availability, skipped if empty. - platforms = [] + hosts = set() + # Set of application groups + app_groups = set() + # Set of specific application names + app_names = set() + # Set of platform availability + platforms = set() + # Set of launch types for which is available + # - if empty then is available for all launch types + # - by default has 'local' which is most common reason for launc hooks + launch_types = {LaunchTypes.local} def __init__(self, launch_context): """Constructor of launch hook. @@ -790,6 +837,10 @@ class LaunchHook: if launch_context.app_name not in cls.app_names: return False + if cls.launch_types: + if launch_context.launch_type not in cls.launch_types: + return False + return True @property @@ -859,9 +910,9 @@ class PostLaunchHook(LaunchHook): class ApplicationLaunchContext: """Context of launching application. - Main purpose of context is to prepare launch arguments and keyword arguments - for new process. Most important part of keyword arguments preparations - are environment variables. + Main purpose of context is to prepare launch arguments and keyword + arguments for new process. Most important part of keyword arguments + preparations are environment variables. During the whole process is possible to use `data` attribute to store object usable in multiple places. @@ -874,14 +925,30 @@ class ApplicationLaunchContext: insert argument between `nuke.exe` and `--NukeX`. To keep them together it is better to wrap them in another list: `[["nuke.exe", "--NukeX"]]`. + Notes: + It is possible to use launch context only to prepare environment + variables. In that case `executable` may be None and can be used + 'run_prelaunch_hooks' method to run prelaunch hooks which prepare + them. + Args: application (Application): Application definition. executable (ApplicationExecutable): Object with path to executable. + env_group (Optional[str]): Environment variable group. If not set + 'DEFAULT_ENV_SUBGROUP' is used. + launch_type (Optional[str]): Launch type. If not set 'local' is used. **data (dict): Any additional data. Data may be used during preparation to store objects usable in multiple places. """ - def __init__(self, application, executable, env_group=None, **data): + def __init__( + self, + application, + executable, + env_group=None, + launch_type=None, + **data + ): from openpype.modules import ModulesManager # Application object @@ -896,6 +963,10 @@ class ApplicationLaunchContext: self.executable = executable + if launch_type is None: + launch_type = LaunchTypes.local + self.launch_type = launch_type + if env_group is None: env_group = DEFAULT_ENV_SUBGROUP @@ -903,8 +974,11 @@ class ApplicationLaunchContext: self.data = dict(data) + launch_args = [] + if executable is not None: + launch_args = executable.as_args() # subprocess.Popen launch arguments (first argument in constructor) - self.launch_args = executable.as_args() + self.launch_args = launch_args self.launch_args.extend(application.arguments) if self.data.get("app_args"): self.launch_args.extend(self.data.pop("app_args")) @@ -946,6 +1020,7 @@ class ApplicationLaunchContext: self.postlaunch_hooks = None self.process = None + self._prelaunch_hooks_executed = False @property def env(self): @@ -1215,6 +1290,27 @@ class ApplicationLaunchContext: # Return process which is already terminated return process + def run_prelaunch_hooks(self): + """Run prelaunch hooks. + + This method will be executed only once, any future calls will skip + the processing. + """ + + if self._prelaunch_hooks_executed: + self.log.warning("Prelaunch hooks were already executed.") + return + # Discover launch hooks + self.discover_launch_hooks() + + # Execute prelaunch hooks + for prelaunch_hook in self.prelaunch_hooks: + self.log.debug("Executing prelaunch hook: {}".format( + str(prelaunch_hook.__class__.__name__) + )) + prelaunch_hook.execute() + self._prelaunch_hooks_executed = True + def launch(self): """Collect data for new process and then create it. @@ -1227,15 +1323,8 @@ class ApplicationLaunchContext: self.log.warning("Application was already launched.") return - # Discover launch hooks - self.discover_launch_hooks() - - # Execute prelaunch hooks - for prelaunch_hook in self.prelaunch_hooks: - self.log.debug("Executing prelaunch hook: {}".format( - str(prelaunch_hook.__class__.__name__) - )) - prelaunch_hook.execute() + if not self._prelaunch_hooks_executed: + self.run_prelaunch_hooks() self.log.debug("All prelaunch hook executed. Starting new process.") @@ -1353,6 +1442,7 @@ def get_app_environments_for_context( task_name, app_name, env_group=None, + launch_type=None, env=None, modules_manager=None ): @@ -1363,54 +1453,33 @@ def get_app_environments_for_context( task_name (str): Name of task. app_name (str): Name of application that is launched and can be found by ApplicationManager. - env (dict): Initial environment variables. `os.environ` is used when - not passed. - modules_manager (ModulesManager): Initialized modules manager. + env_group (Optional[str]): Name of environment group. If not passed + default group is used. + launch_type (Optional[str]): Type for which prelaunch hooks are + executed. + env (Optional[dict[str, str]]): Initial environment variables. + `os.environ` is used when not passed. + modules_manager (Optional[ModulesManager]): Initialized modules + manager. Returns: dict: Environments for passed context and application. """ - from openpype.modules import ModulesManager - from openpype.pipeline import Anatomy - from openpype.lib.openpype_version import is_running_staging - - # Project document - project_doc = get_project(project_name) - asset_doc = get_asset_by_name(project_name, asset_name) - - if modules_manager is None: - modules_manager = ModulesManager() - - # Prepare app object which can be obtained only from ApplciationManager + # Prepare app object which can be obtained only from ApplicationManager app_manager = ApplicationManager() - app = app_manager.applications[app_name] - - # Project's anatomy - anatomy = Anatomy(project_name) - - data = EnvironmentPrepData({ - "project_name": project_name, - "asset_name": asset_name, - "task_name": task_name, - - "app": app, - - "project_doc": project_doc, - "asset_doc": asset_doc, - - "anatomy": anatomy, - - "env": env - }) - data["env"].update(anatomy.root_environments()) - if is_running_staging(): - data["env"]["OPENPYPE_IS_STAGING"] = "1" - - prepare_app_environments(data, env_group, modules_manager) - prepare_context_environments(data, env_group, modules_manager) - - return data["env"] + context = app_manager.create_launch_context( + app_name, + project_name=project_name, + asset_name=asset_name, + task_name=task_name, + env_group=env_group, + launch_type=launch_type, + env=env, + modules_manager=modules_manager, + ) + context.run_prelaunch_hooks() + return context.env def _merge_env(env, current_env): diff --git a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py index 86ecffd5b8..ac4e499e41 100644 --- a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py +++ b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py @@ -2,11 +2,12 @@ import os import ftrack_api from openpype.settings import get_project_settings -from openpype.lib import PostLaunchHook +from openpype.lib.applications import PostLaunchHook, LaunchTypes class PostFtrackHook(PostLaunchHook): order = None + launch_types = {LaunchTypes.local} def execute(self): project_name = self.data.get("project_name") diff --git a/openpype/modules/slack/launch_hooks/pre_python2_vendor.py b/openpype/modules/slack/launch_hooks/pre_python2_vendor.py index 0f4bc22a34..891c92bb7a 100644 --- a/openpype/modules/slack/launch_hooks/pre_python2_vendor.py +++ b/openpype/modules/slack/launch_hooks/pre_python2_vendor.py @@ -1,5 +1,5 @@ import os -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook from openpype_modules.slack import SLACK_MODULE_DIR @@ -8,6 +8,7 @@ class PrePython2Support(PreLaunchHook): Path to vendor modules is added to the beginning of PYTHONPATH. """ + launch_types = set() def execute(self): if not self.application.use_python_2: diff --git a/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py index bbc220945c..77f6933756 100644 --- a/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py +++ b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py @@ -1,12 +1,8 @@ import os import shutil -from openpype.client.entities import ( - get_representations, - get_project -) - -from openpype.lib import PreLaunchHook +from openpype.client.entities import get_representations +from openpype.lib.applications import PreLaunchHook, LaunchTypes from openpype.lib.profiles_filtering import filter_profiles from openpype.modules.sync_server.sync_server import ( download_last_published_workfile, @@ -32,6 +28,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): "nuke", "nukeassist", "nukex", "hiero", "nukestudio", "maya", "harmony", "celaction", "flame", "fusion", "houdini", "tvpaint"] + launch_types = {LaunchTypes.local} def execute(self): """Check if local workfile doesn't exist, else copy it. diff --git a/openpype/modules/timers_manager/launch_hooks/post_start_timer.py b/openpype/modules/timers_manager/launch_hooks/post_start_timer.py index d6ae013403..76c3cca33e 100644 --- a/openpype/modules/timers_manager/launch_hooks/post_start_timer.py +++ b/openpype/modules/timers_manager/launch_hooks/post_start_timer.py @@ -1,4 +1,4 @@ -from openpype.lib import PostLaunchHook +from openpype.lib.applications import PostLaunchHook, LaunchTypes class PostStartTimerHook(PostLaunchHook): @@ -7,6 +7,7 @@ class PostStartTimerHook(PostLaunchHook): This module requires enabled TimerManager module. """ order = None + launch_types = {LaunchTypes.local} def execute(self): project_name = self.data.get("project_name") diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 8a3f25a026..4cb4b97707 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -88,7 +88,10 @@ class PypeCommands: """ from openpype.lib import Logger - from openpype.lib.applications import get_app_environments_for_context + from openpype.lib.applications import ( + get_app_environments_for_context, + LaunchTypes, + ) from openpype.modules import ModulesManager from openpype.pipeline import ( install_openpype_plugins, @@ -122,7 +125,8 @@ class PypeCommands: context["project_name"], context["asset_name"], context["task_name"], - app_full_name + app_full_name, + launch_type=LaunchTypes.farm_publish, ) os.environ.update(env) @@ -237,11 +241,19 @@ class PypeCommands: Called by Deadline plugin to propagate environment into render jobs. """ - from openpype.lib.applications import get_app_environments_for_context + from openpype.lib.applications import ( + get_app_environments_for_context, + LaunchTypes, + ) if all((project, asset, task, app)): env = get_app_environments_for_context( - project, asset, task, app, env_group + project, + asset, + task, + app, + env_group=env_group, + launch_type=LaunchTypes.farm_render, ) else: env = os.environ.copy() From a4660f4d6ccbb0f2c3a9197b52da9a797fc758d9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 3 Aug 2023 16:06:36 +0800 Subject: [PATCH 424/446] use the empty modifiers in container to store OP/AYON Parameter --- openpype/hosts/max/api/plugin.py | 8 +++++--- openpype/hosts/max/plugins/publish/collect_members.py | 2 +- openpype/hosts/unreal/integration | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index d8db716e6d..7b93a1a7cf 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -183,9 +183,11 @@ class MaxCreatorBase(object): """ if isinstance(node, str): node = rt.Container(name=node) - attrs = rt.Execute(MS_CUSTOM_ATTRIB) - rt.custAttributes.add(node.baseObject, attrs) + modifier = rt.EmptyModifier() + rt.addModifier(node, modifier) + node.modifiers[0].name = "OP Data" + rt.custAttributes.add(node.modifiers[0], attrs) return node @@ -215,7 +217,7 @@ class MaxCreator(Creator, MaxCreatorBase): # Setting the property rt.setProperty( - instance_node.openPypeData, "all_handles", node_list) + instance_node.modifiers[0].openPypeData, "all_handles", node_list) self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) diff --git a/openpype/hosts/max/plugins/publish/collect_members.py b/openpype/hosts/max/plugins/publish/collect_members.py index 812d82ff26..2970cf0e24 100644 --- a/openpype/hosts/max/plugins/publish/collect_members.py +++ b/openpype/hosts/max/plugins/publish/collect_members.py @@ -17,6 +17,6 @@ class CollectMembers(pyblish.api.InstancePlugin): container = rt.GetNodeByName(instance.data["instance_node"]) instance.data["members"] = [ member.node for member - in container.openPypeData.all_handles + in container.modifiers[0].openPypeData.all_handles ] self.log.debug("{}".format(instance.data["members"])) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index 63266607ce..ff15c70077 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit 63266607ceb972a61484f046634ddfc9eb0b5757 +Subproject commit ff15c700771e719cc5f3d561ac5d6f7590623986 From 20f1f99f9ebe1b9ace17e4e1484d41484b1dfe90 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 3 Aug 2023 16:17:56 +0800 Subject: [PATCH 425/446] hound shut --- openpype/hosts/max/api/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 7b93a1a7cf..9d36e36ccb 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -217,7 +217,8 @@ class MaxCreator(Creator, MaxCreatorBase): # Setting the property rt.setProperty( - instance_node.modifiers[0].openPypeData, "all_handles", node_list) + instance_node.modifiers[0].openPypeData, + "all_handles", node_list) self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) From 20376655faba25002d4e0bc0f31e2c4cd0bd2bb3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Aug 2023 10:24:00 +0200 Subject: [PATCH 426/446] use relative path to MAX_HOST_DIR constant (#5382) --- openpype/hosts/max/hooks/force_startup_script.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/hooks/force_startup_script.py b/openpype/hosts/max/hooks/force_startup_script.py index 701e348293..64ce46336f 100644 --- a/openpype/hosts/max/hooks/force_startup_script.py +++ b/openpype/hosts/max/hooks/force_startup_script.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Pre-launch to force 3ds max startup script.""" import os +from openpype.hosts.max import MAX_HOST_DIR from openpype.lib.applications import PreLaunchHook, LaunchTypes @@ -21,5 +22,6 @@ class ForceStartupScript(PreLaunchHook): startup_args = [ "-U", "MAXScript", - f"{os.getenv('OPENPYPE_ROOT')}\\openpype\\hosts\\max\\startup\\startup.ms"] # noqa + os.path.join(MAX_HOST_DIR, "startup", "startup.ms"), + ] self.launch_context.launch_args.append(startup_args) From 9e008a80e0cb45f25d06f29d626e5657e199cf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 3 Aug 2023 10:33:09 +0200 Subject: [PATCH 427/446] Update openpype/hosts/nuke/api/plugin.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/api/plugin.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 4a7bb03216..18e48ec79d 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -215,15 +215,10 @@ class NukeCreator(NewCreator): for created_inst, changes in update_list: instance_node = created_inst.transient_data["node"] - changed_keys = { - key: changes[key].new_value - for key in changes.changed_keys - } - # update instance node name if subset name changed - if "subset" in changed_keys: + if "subset" in changes: instance_node["name"].setValue( - changed_keys["subset"] + changes["subset"].new_value ) # in case node is not existing anymore (user erased it manually) From 9a8a16eed89d321c631aff3bd1379700afbd901c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Aug 2023 10:35:36 +0200 Subject: [PATCH 428/446] use better list to check from --- openpype/hosts/nuke/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 18e48ec79d..85a4046823 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -216,7 +216,7 @@ class NukeCreator(NewCreator): instance_node = created_inst.transient_data["node"] # update instance node name if subset name changed - if "subset" in changes: + if "subset" in changes.changed_keys: instance_node["name"].setValue( changes["subset"].new_value ) From bee48e9fbfc669163fc521e2a4b180a1d28cb420 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 3 Aug 2023 16:41:13 +0800 Subject: [PATCH 429/446] resolve unrelated codes --- openpype/hosts/unreal/integration | 2 +- tools/modules/powershell/PSWriteColor | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index ff15c70077..63266607ce 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit ff15c700771e719cc5f3d561ac5d6f7590623986 +Subproject commit 63266607ceb972a61484f046634ddfc9eb0b5757 diff --git a/tools/modules/powershell/PSWriteColor b/tools/modules/powershell/PSWriteColor index 12eda384eb..5941ee3803 160000 --- a/tools/modules/powershell/PSWriteColor +++ b/tools/modules/powershell/PSWriteColor @@ -1 +1 @@ -Subproject commit 12eda384ebd7a7954e15855e312215c009c97114 +Subproject commit 5941ee380367693bcd52dfe269f63ed4120df900 From ed3e008781be9fb6d48b571b19ad976365671cf0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 3 Aug 2023 16:49:03 +0800 Subject: [PATCH 430/446] resovled the code --- openpype/hosts/unreal/integration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index 63266607ce..ff15c70077 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit 63266607ceb972a61484f046634ddfc9eb0b5757 +Subproject commit ff15c700771e719cc5f3d561ac5d6f7590623986 From 5d8ac1d63757d806e992c9faefb2f4b25345ee35 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 3 Aug 2023 16:55:26 +0800 Subject: [PATCH 431/446] resolve unrelated codes --- openpype/hosts/max/api/plugin.py | 5 +++-- openpype/hosts/max/plugins/publish/collect_members.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 9d36e36ccb..670c3ba860 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -183,6 +183,7 @@ class MaxCreatorBase(object): """ if isinstance(node, str): node = rt.Container(name=node) + attrs = rt.Execute(MS_CUSTOM_ATTRIB) modifier = rt.EmptyModifier() rt.addModifier(node, modifier) @@ -257,8 +258,8 @@ class MaxCreator(Creator, MaxCreatorBase): instance_node = rt.GetNodeByName( instance.data.get("instance_node")) if instance_node: - count = rt.custAttributes.count(instance_node) - rt.custAttributes.delete(instance_node, count) + count = rt.custAttributes.count(instance_node.modifiers[0]) + rt.custAttributes.delete(instance_node.modifiers[0], count) rt.Delete(instance_node) self._remove_instance_from_context(instance) diff --git a/openpype/hosts/max/plugins/publish/collect_members.py b/openpype/hosts/max/plugins/publish/collect_members.py index 2970cf0e24..4efd92dd70 100644 --- a/openpype/hosts/max/plugins/publish/collect_members.py +++ b/openpype/hosts/max/plugins/publish/collect_members.py @@ -17,6 +17,6 @@ class CollectMembers(pyblish.api.InstancePlugin): container = rt.GetNodeByName(instance.data["instance_node"]) instance.data["members"] = [ member.node for member - in container.modifiers[0].openPypeData.all_handles + in container.modifiers[0].openPypeData.all_handles ] self.log.debug("{}".format(instance.data["members"])) From e230b3a66dd080c836924ad88fd2f830a4497bcb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 3 Aug 2023 16:56:19 +0800 Subject: [PATCH 432/446] resolve unrelated codes --- openpype/hosts/unreal/integration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index ff15c70077..63266607ce 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit ff15c700771e719cc5f3d561ac5d6f7590623986 +Subproject commit 63266607ceb972a61484f046634ddfc9eb0b5757 From 188c6f64b08b9953a0d8b5f61b3b29ec84b08dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 3 Aug 2023 10:58:52 +0200 Subject: [PATCH 433/446] Bugfix: Dependency without 'inputLinks' not downloaded (#5337) * Bugfix: Dependency without 'inputLinks' not downloaded * cleaning --- openpype/client/mongo/entity_links.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/client/mongo/entity_links.py b/openpype/client/mongo/entity_links.py index c97a828118..fd13a2d83b 100644 --- a/openpype/client/mongo/entity_links.py +++ b/openpype/client/mongo/entity_links.py @@ -212,16 +212,12 @@ def _process_referenced_pipeline_result(result, link_type): continue for output in sorted(outputs_recursive, key=lambda o: o["depth"]): - output_links = output.get("data", {}).get("inputLinks") - if not output_links and output["type"] != "hero_version": - continue - # Leaf if output["_id"] not in correctly_linked_ids: continue _filter_input_links( - output_links, + output.get("data", {}).get("inputLinks"), link_type, correctly_linked_ids ) From 5da9e65975171b31d599dd15c065ac4f518e067d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Aug 2023 10:59:39 +0200 Subject: [PATCH 434/446] removed unused imports from AE extractor (#5397) --- .../aftereffects/plugins/publish/extract_local_render.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index c70aa41dbe..bdb48e11f8 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -1,11 +1,5 @@ import os -import sys -import six -from openpype.lib import ( - get_ffmpeg_tool_path, - run_subprocess, -) from openpype.pipeline import publish from openpype.hosts.aftereffects.api import get_stub From 8130699bd81ee49c5800b501c18f48faa19343bf Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 3 Aug 2023 17:02:10 +0800 Subject: [PATCH 435/446] resolve unrelated codes --- openpype/hosts/max/plugins/publish/collect_members.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/collect_members.py b/openpype/hosts/max/plugins/publish/collect_members.py index 4efd92dd70..2970cf0e24 100644 --- a/openpype/hosts/max/plugins/publish/collect_members.py +++ b/openpype/hosts/max/plugins/publish/collect_members.py @@ -17,6 +17,6 @@ class CollectMembers(pyblish.api.InstancePlugin): container = rt.GetNodeByName(instance.data["instance_node"]) instance.data["members"] = [ member.node for member - in container.modifiers[0].openPypeData.all_handles + in container.modifiers[0].openPypeData.all_handles ] self.log.debug("{}".format(instance.data["members"])) From 6f376d39163fc981526a163d8fd9f0001865812d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Aug 2023 11:29:36 +0200 Subject: [PATCH 436/446] nuke: put Workfile builder on deprication also fix the workfile calback loop --- openpype/hosts/nuke/api/lib.py | 37 ++++++++++--------- openpype/hosts/nuke/api/pipeline.py | 10 ++++- .../projects_schema/schema_project_nuke.json | 4 ++ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 364c8eeff4..6b88cbcf34 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -424,10 +424,13 @@ def add_publish_knob(node): return node -@deprecated +@deprecated("openpype.hosts.nuke.api.lib.set_node_data") def set_avalon_knob_data(node, data=None, prefix="avalon:"): """[DEPRECATED] Sets data into nodes's avalon knob + This function is still used but soon will be deprecated. + Use `set_node_data` instead. + Arguments: node (nuke.Node): Nuke node to imprint with data, data (dict, optional): Data to be imprinted into AvalonTab @@ -487,10 +490,13 @@ def set_avalon_knob_data(node, data=None, prefix="avalon:"): return node -@deprecated +@deprecated("openpype.hosts.nuke.api.lib.get_node_data") def get_avalon_knob_data(node, prefix="avalon:", create=True): """[DEPRECATED] Gets a data from nodes's avalon knob + This function is still used but soon will be deprecated. + Use `get_node_data` instead. + Arguments: node (obj): Nuke node to search for data, prefix (str, optional): filtering prefix @@ -2204,7 +2210,6 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. continue preset_clrsp = input["colorspace"] - log.debug(preset_clrsp) if preset_clrsp is not None: current = n["colorspace"].value() future = str(preset_clrsp) @@ -2686,7 +2691,15 @@ def _launch_workfile_app(): host_tools.show_workfiles(parent=None, on_top=True) +@deprecated("openpype.hosts.nuke.api.lib.start_workfile_template_builder") def process_workfile_builder(): + """ [DEPRECATED] Process workfile builder on nuke start + + This function is deprecated and will be removed in future versions. + Use settings for `project_settings/nuke/templated_workfile_build` which are + supported by api `start_workfile_template_builder()`. + """ + # to avoid looping of the callback, remove it! nuke.removeOnCreate(process_workfile_builder, nodeClass="Root") @@ -2695,11 +2708,6 @@ def process_workfile_builder(): workfile_builder = project_settings["nuke"].get( "workfile_builder", {}) - # get all imortant settings - openlv_on = env_value_to_bool( - env_key="AVALON_OPEN_LAST_WORKFILE", - default=None) - # get settings createfv_on = workfile_builder.get("create_first_version") or None builder_on = workfile_builder.get("builder_on_start") or None @@ -2740,20 +2748,15 @@ def process_workfile_builder(): save_file(last_workfile_path) return - # skip opening of last version if it is not enabled - if not openlv_on or not os.path.exists(last_workfile_path): - return - - log.info("Opening last workfile...") - # open workfile - open_file(last_workfile_path) - def start_workfile_template_builder(): from .workfile_template_builder import ( build_workfile_template ) + # remove callback since it would be duplicating the workfile + nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root") + # to avoid looping of the callback, remove it! log.info("Starting workfile template builder...") try: @@ -2761,8 +2764,6 @@ def start_workfile_template_builder(): except TemplateProfileNotFound: log.warning("Template profile not found. Skipping...") - # remove callback since it would be duplicating the workfile - nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root") @deprecated def recreate_instance(origin_node, avalon_data=None): diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index a48ae0032a..c6bdd5feaf 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -34,6 +34,7 @@ from .lib import ( get_main_window, add_publish_knob, WorkfileSettings, + # TODO: remove this once workfile builder will be removed process_workfile_builder, start_workfile_template_builder, launch_workfiles_app, @@ -159,8 +160,14 @@ def add_nuke_callbacks(): # Set context settings. nuke.addOnCreate( workfile_settings.set_context_settings, nodeClass="Root") + + # adding favorites to file browser nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root") + + # template builder callbacks nuke.addOnCreate(start_workfile_template_builder, nodeClass="Root") + + # TODO: remove this callback once workfile builder will be removed nuke.addOnCreate(process_workfile_builder, nodeClass="Root") # fix ffmpeg settings on script @@ -170,9 +177,10 @@ def add_nuke_callbacks(): nuke.addOnScriptLoad(check_inventory_versions) nuke.addOnScriptSave(check_inventory_versions) - # # set apply all workfile settings on script load and save + # set apply all workfile settings on script load and save nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) + if nuke_settings["nuke-dirmap"]["enabled"]: log.info("Added Nuke's dir-mapping callback ...") # Add dirmap for file paths. diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 26c64e6219..6b516ddf4a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -284,6 +284,10 @@ "type": "schema_template", "name": "template_workfile_options" }, + { + "type": "label", + "label": "^ Settings and for Workfile Builder is deprecated and will be soon removed.
Please use Template Workfile Build Settings instead." + }, { "type": "schema", "name": "schema_templated_workfile_build" From 18f891a3f9436dc6f056bed86fd309bb56f56ac0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 3 Aug 2023 17:48:32 +0800 Subject: [PATCH 437/446] resolve submodule conflict --- tools/modules/powershell/PSWriteColor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/modules/powershell/PSWriteColor b/tools/modules/powershell/PSWriteColor index 5941ee3803..12eda384eb 160000 --- a/tools/modules/powershell/PSWriteColor +++ b/tools/modules/powershell/PSWriteColor @@ -1 +1 @@ -Subproject commit 5941ee380367693bcd52dfe269f63ed4120df900 +Subproject commit 12eda384ebd7a7954e15855e312215c009c97114 From 3ae020f064feb9eb9ee61576ab9a1a9264f1433b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:24:23 +0200 Subject: [PATCH 438/446] Applications: Launch hooks cleanup (#5395) * ApplicationManager can have more granular way how applications are launched * executable is optional to be able create ApplicationLaunchContext * launch context can run prelaunch hooks without launching application * 'get_app_environments_for_context' is using launch context to prepare environments * added 'launch_type' as one of filtering options for LaunchHook * added 'local' launch type filter to existing launch hooks * define 'automated' launch type in remote publish function * modified publish and extract environments cli commands * launch types are only for local by default * fix import * fix launch types of global host data * change order or kwargs * change unreal filter attribute * use set instead of list * removed '__init__' from celaction hooks * use 'CELACTION_ROOT_DIR' in pre setup * use full import from applications --- openpype/hooks/pre_add_last_workfile_arg.py | 6 +++--- openpype/hooks/pre_copy_template_workfile.py | 2 +- openpype/hooks/pre_foundry_apps.py | 4 ++-- openpype/hooks/pre_mac_launch.py | 2 +- openpype/hooks/pre_non_python_host_launch.py | 2 +- openpype/hooks/pre_ocio_hook.py | 12 +++++------- .../hosts/blender/hooks/pre_windows_console.py | 4 ++-- openpype/hosts/celaction/hooks/__init__.py | 0 .../celaction/hooks/pre_celaction_setup.py | 17 +++++++---------- openpype/hosts/flame/hooks/pre_flame_setup.py | 2 +- .../fusion/hooks/pre_fusion_profile_hook.py | 2 +- openpype/hosts/fusion/hooks/pre_fusion_setup.py | 2 +- openpype/hosts/houdini/hooks/set_paths.py | 2 +- .../hosts/max/hooks/force_startup_script.py | 2 +- openpype/hosts/max/hooks/inject_python.py | 2 +- openpype/hosts/max/hooks/set_paths.py | 2 +- .../hosts/maya/hooks/pre_auto_load_plugins.py | 2 +- openpype/hosts/maya/hooks/pre_copy_mel.py | 2 +- .../pre_open_workfile_post_initialization.py | 2 +- .../hosts/nuke/hooks/pre_nukeassist_setup.py | 2 +- .../resolve/hooks/pre_resolve_last_workfile.py | 2 +- .../hosts/resolve/hooks/pre_resolve_setup.py | 2 +- .../hosts/resolve/hooks/pre_resolve_startup.py | 2 +- openpype/hosts/tvpaint/hooks/pre_launch_args.py | 2 +- 24 files changed, 37 insertions(+), 42 deletions(-) delete mode 100644 openpype/hosts/celaction/hooks/__init__.py diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 0e43f1bfe6..c160d8e062 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -13,7 +13,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): # Execute after workfile template copy order = 10 - app_groups = [ + app_groups = { "3dsmax", "maya", "nuke", @@ -26,8 +26,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "photoshop", "tvpaint", "substancepainter", - "aftereffects" - ] + "aftereffects", + } launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hooks/pre_copy_template_workfile.py b/openpype/hooks/pre_copy_template_workfile.py index 9962dabdd8..2203ff4396 100644 --- a/openpype/hooks/pre_copy_template_workfile.py +++ b/openpype/hooks/pre_copy_template_workfile.py @@ -19,7 +19,7 @@ class CopyTemplateWorkfile(PreLaunchHook): # Before `AddLastWorkfileToLaunchArgs` order = 0 - app_groups = ["blender", "photoshop", "tvpaint", "aftereffects"] + app_groups = {"blender", "photoshop", "tvpaint", "aftereffects"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 50e50e74a2..7536df4c16 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -13,8 +13,8 @@ class LaunchFoundryAppsWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = ["nuke", "nukeassist", "nukex", "hiero", "nukestudio"] - platforms = ["windows"] + app_groups = {"nuke", "nukeassist", "nukex", "hiero", "nukestudio"} + platforms = {"windows"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hooks/pre_mac_launch.py b/openpype/hooks/pre_mac_launch.py index 298346c9b1..402e9a5517 100644 --- a/openpype/hooks/pre_mac_launch.py +++ b/openpype/hooks/pre_mac_launch.py @@ -12,7 +12,7 @@ class LaunchWithTerminal(PreLaunchHook): """ order = 1000 - platforms = ["darwin"] + platforms = {"darwin"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py index e58c354360..d9e912c826 100644 --- a/openpype/hooks/pre_non_python_host_launch.py +++ b/openpype/hooks/pre_non_python_host_launch.py @@ -17,7 +17,7 @@ class NonPythonHostHook(PreLaunchHook): python script which launch the host. For these cases it is necessary to prepend python (or openpype) executable and script path before application's. """ - app_groups = ["harmony", "photoshop", "aftereffects"] + app_groups = {"harmony", "photoshop", "aftereffects"} order = 20 launch_types = {LaunchTypes.local} diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index 7c53d3db66..1ac305b635 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -1,8 +1,6 @@ -from openpype.lib import PreLaunchHook +from openpype.lib.applications import PreLaunchHook -from openpype.pipeline.colorspace import ( - get_imageio_config -) +from openpype.pipeline.colorspace import get_imageio_config from openpype.pipeline.template_data import get_template_data_with_names @@ -10,7 +8,7 @@ class OCIOEnvHook(PreLaunchHook): """Set OCIO environment variable for hosts that use OpenColorIO.""" order = 0 - hosts = [ + hosts = { "substancepainter", "fusion", "blender", @@ -20,8 +18,8 @@ class OCIOEnvHook(PreLaunchHook): "maya", "nuke", "hiero", - "resolve" - ] + "resolve", + } launch_types = set() def execute(self): diff --git a/openpype/hosts/blender/hooks/pre_windows_console.py b/openpype/hosts/blender/hooks/pre_windows_console.py index c6ecf284ef..2161b7a2f5 100644 --- a/openpype/hosts/blender/hooks/pre_windows_console.py +++ b/openpype/hosts/blender/hooks/pre_windows_console.py @@ -13,8 +13,8 @@ class BlenderConsoleWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = ["blender"] - platforms = ["windows"] + app_groups = {"blender"} + platforms = {"windows"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/celaction/hooks/__init__.py b/openpype/hosts/celaction/hooks/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openpype/hosts/celaction/hooks/pre_celaction_setup.py b/openpype/hosts/celaction/hooks/pre_celaction_setup.py index df27195e60..83aeab7c58 100644 --- a/openpype/hosts/celaction/hooks/pre_celaction_setup.py +++ b/openpype/hosts/celaction/hooks/pre_celaction_setup.py @@ -4,19 +4,15 @@ import winreg import subprocess from openpype.lib import get_openpype_execute_args from openpype.lib.applications import PreLaunchHook, LaunchTypes -from openpype.hosts.celaction import scripts - -CELACTION_SCRIPTS_DIR = os.path.dirname( - os.path.abspath(scripts.__file__) -) +from openpype.hosts.celaction import CELACTION_ROOT_DIR class CelactionPrelaunchHook(PreLaunchHook): """ Bootstrap celacion with pype """ - app_groups = ["celaction"] - platforms = ["windows"] + app_groups = {"celaction"} + platforms = {"windows"} launch_types = {LaunchTypes.local} def execute(self): @@ -39,7 +35,9 @@ class CelactionPrelaunchHook(PreLaunchHook): winreg.KEY_ALL_ACCESS ) - path_to_cli = os.path.join(CELACTION_SCRIPTS_DIR, "publish_cli.py") + path_to_cli = os.path.join( + CELACTION_ROOT_DIR, "scripts", "publish_cli.py" + ) subprocess_args = get_openpype_execute_args("run", path_to_cli) openpype_executable = subprocess_args.pop(0) workfile_settings = self.get_workfile_settings() @@ -124,9 +122,8 @@ class CelactionPrelaunchHook(PreLaunchHook): if not os.path.exists(workfile_path): # TODO add ability to set different template workfile path via # settings - openpype_celaction_dir = os.path.dirname(CELACTION_SCRIPTS_DIR) template_path = os.path.join( - openpype_celaction_dir, + CELACTION_ROOT_DIR, "resources", "celaction_template_scene.scn" ) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 61e3200d89..850569cfdd 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -19,7 +19,7 @@ class FlamePrelaunch(PreLaunchHook): Will make sure flame_script_dirs are copied to user's folder defined in environment var FLAME_SCRIPT_DIR. """ - app_groups = ["flame"] + app_groups = {"flame"} permissions = 0o777 wtc_script_path = os.path.join( diff --git a/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py b/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py index da74f8e1fe..66b0f803aa 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py @@ -25,7 +25,7 @@ class FusionCopyPrefsPrelaunch(PreLaunchHook): Master.prefs is defined in openpype/hosts/fusion/deploy/fusion_shared.prefs """ - app_groups = ["fusion"] + app_groups = {"fusion"} order = 2 launch_types = {LaunchTypes.local} diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index 68ef23d520..576628e876 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -21,7 +21,7 @@ class FusionPrelaunch(PreLaunchHook): Fusion 18 : Python 3.6 - 3.10 """ - app_groups = ["fusion"] + app_groups = {"fusion"} order = 1 launch_types = {LaunchTypes.local} diff --git a/openpype/hosts/houdini/hooks/set_paths.py b/openpype/hosts/houdini/hooks/set_paths.py index 2e7bf51757..b23659e23b 100644 --- a/openpype/hosts/houdini/hooks/set_paths.py +++ b/openpype/hosts/houdini/hooks/set_paths.py @@ -6,7 +6,7 @@ class SetPath(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ - app_groups = ["houdini"] + app_groups = {"houdini"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/max/hooks/force_startup_script.py b/openpype/hosts/max/hooks/force_startup_script.py index 64ce46336f..d87697b819 100644 --- a/openpype/hosts/max/hooks/force_startup_script.py +++ b/openpype/hosts/max/hooks/force_startup_script.py @@ -14,7 +14,7 @@ class ForceStartupScript(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ - app_groups = ["3dsmax"] + app_groups = {"3dsmax"} order = 11 launch_types = {LaunchTypes.local} diff --git a/openpype/hosts/max/hooks/inject_python.py b/openpype/hosts/max/hooks/inject_python.py index bbfc95c078..874884585e 100644 --- a/openpype/hosts/max/hooks/inject_python.py +++ b/openpype/hosts/max/hooks/inject_python.py @@ -13,7 +13,7 @@ class InjectPythonPath(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ - app_groups = ["3dsmax"] + app_groups = {"3dsmax"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/max/hooks/set_paths.py b/openpype/hosts/max/hooks/set_paths.py index f06efff7c8..4b961fa91e 100644 --- a/openpype/hosts/max/hooks/set_paths.py +++ b/openpype/hosts/max/hooks/set_paths.py @@ -6,7 +6,7 @@ class SetPath(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ - app_groups = ["max"] + app_groups = {"max"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py index 0437b6fd9d..4b1ea698a6 100644 --- a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py +++ b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py @@ -6,7 +6,7 @@ class MayaPreAutoLoadPlugins(PreLaunchHook): # Before AddLastWorkfileToLaunchArgs order = 9 - app_groups = ["maya"] + app_groups = {"maya"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/maya/hooks/pre_copy_mel.py b/openpype/hosts/maya/hooks/pre_copy_mel.py index ebb0c521c9..0fb5af149a 100644 --- a/openpype/hosts/maya/hooks/pre_copy_mel.py +++ b/openpype/hosts/maya/hooks/pre_copy_mel.py @@ -7,7 +7,7 @@ class PreCopyMel(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ - app_groups = ["maya"] + app_groups = {"maya"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py index 0c1fd0efe3..1fe3c3ca2c 100644 --- a/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py +++ b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py @@ -6,7 +6,7 @@ class MayaPreOpenWorkfilePostInitialization(PreLaunchHook): # Before AddLastWorkfileToLaunchArgs. order = 9 - app_groups = ["maya"] + app_groups = {"maya"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py index bdb271e3f1..657291ec51 100644 --- a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py +++ b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py @@ -5,7 +5,7 @@ class PrelaunchNukeAssistHook(PreLaunchHook): """ Adding flag when nukeassist """ - app_groups = ["nukeassist"] + app_groups = {"nukeassist"} launch_types = set() def execute(self): diff --git a/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py index dc986ec1d2..73f5ac75b1 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py @@ -9,7 +9,7 @@ class PreLaunchResolveLastWorkfile(PreLaunchHook): workfile. This property is set explicitly in Launcher. """ order = 10 - app_groups = ["resolve"] + app_groups = {"resolve"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 389256f4da..326f37dffc 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -30,7 +30,7 @@ class PreLaunchResolveSetup(PreLaunchHook): """ - app_groups = ["resolve"] + app_groups = {"resolve"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/resolve/hooks/pre_resolve_startup.py b/openpype/hosts/resolve/hooks/pre_resolve_startup.py index 649af817ae..6dbfd09a37 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_startup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_startup.py @@ -9,7 +9,7 @@ class PreLaunchResolveStartup(PreLaunchHook): """ order = 11 - app_groups = ["resolve"] + app_groups = {"resolve"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/hosts/tvpaint/hooks/pre_launch_args.py b/openpype/hosts/tvpaint/hooks/pre_launch_args.py index 065da316ab..a1c946b60b 100644 --- a/openpype/hosts/tvpaint/hooks/pre_launch_args.py +++ b/openpype/hosts/tvpaint/hooks/pre_launch_args.py @@ -11,7 +11,7 @@ class TvpaintPrelaunchHook(PreLaunchHook): Existence of last workfile is checked. If workfile does not exists tries to copy templated workfile from predefined path. """ - app_groups = ["tvpaint"] + app_groups = {"tvpaint"} launch_types = {LaunchTypes.local} def execute(self): From 3ba5f1ce6236f5e3fd4da1cf66cfc138e2c70b95 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Aug 2023 14:23:58 +0200 Subject: [PATCH 439/446] adding BBox knob type to settings also fixing some typos --- openpype/hosts/nuke/api/lib.py | 14 ++--- .../schemas/template_nuke_knob_inputs.json | 58 +++++++++++++++++-- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 364c8eeff4..a42983b32e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1699,7 +1699,7 @@ def create_write_node_legacy( knob_value = float(knob_value) if knob_type == "bool": knob_value = bool(knob_value) - if knob_type in ["2d_vector", "3d_vector"]: + if knob_type in ["2d_vector", "3d_vector", "color", "box"]: knob_value = list(knob_value) GN[knob_name].setValue(knob_value) @@ -1715,7 +1715,7 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): Args: node (nuke.Node): nuke node knob_settings (list): list of dict. Keys are `type`, `name`, `value` - kwargs (dict)[optional]: keys for formatable knob settings + kwargs (dict)[optional]: keys for formattable knob settings """ for knob in knob_settings: log.debug("__ knob: {}".format(pformat(knob))) @@ -1732,7 +1732,7 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): ) continue - # first deal with formatable knob settings + # first deal with formattable knob settings if knob_type == "formatable": template = knob["template"] to_type = knob["to_type"] @@ -1741,8 +1741,8 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): **kwargs ) except KeyError as msg: - log.warning("__ msg: {}".format(msg)) - raise KeyError(msg) + raise KeyError( + "Not able to format expression: {}".format(msg)) # convert value to correct type if to_type == "2d_vector": @@ -1781,8 +1781,8 @@ def convert_knob_value_to_correct_type(knob_type, knob_value): knob_value = knob_value elif knob_type == "color_gui": knob_value = color_gui_to_int(knob_value) - elif knob_type in ["2d_vector", "3d_vector", "color"]: - knob_value = [float(v) for v in knob_value] + elif knob_type in ["2d_vector", "3d_vector", "color", "box"]: + knob_value = [float(val_) for val_ in knob_value] return knob_value diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json index c9dee8681a..51c78ce8f0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json @@ -213,7 +213,7 @@ }, { "type": "number", - "key": "y", + "key": "z", "default": 1, "decimal": 4, "maximum": 99999999 @@ -238,29 +238,75 @@ "object_types": [ { "type": "number", - "key": "x", + "key": "r", "default": 1, "decimal": 4, "maximum": 99999999 }, { "type": "number", - "key": "x", + "key": "g", "default": 1, "decimal": 4, "maximum": 99999999 }, + { + "type": "number", + "key": "b", + "default": 1, + "decimal": 4, + "maximum": 99999999 + }, + { + "type": "number", + "key": "a", + "default": 1, + "decimal": 4, + "maximum": 99999999 + } + ] + } + ] + }, + { + "key": "box", + "label": "Box", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "list-strict", + "key": "value", + "label": "Value", + "object_types": [ + { + "type": "number", + "key": "x", + "default": 0, + "decimal": 4, + "maximum": 99999999 + }, { "type": "number", "key": "y", - "default": 1, + "default": 0, "decimal": 4, "maximum": 99999999 }, { "type": "number", - "key": "y", - "default": 1, + "key": "r", + "default": 1920, + "decimal": 4, + "maximum": 99999999 + }, + { + "type": "number", + "key": "t", + "default": 1080, "decimal": 4, "maximum": 99999999 } From c9cf6646f78223f492f21b8097f2b73ae6df6b64 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Aug 2023 15:08:19 +0200 Subject: [PATCH 440/446] AYON: 3dsMax settings (#5401) * create copy of 3dsmax settings instead of removing it * keep '3dsmax' as 'adsk_3dsmax' --- openpype/hooks/pre_add_last_workfile_arg.py | 2 +- openpype/hosts/max/hooks/force_startup_script.py | 2 +- openpype/hosts/max/hooks/inject_python.py | 2 +- openpype/settings/ayon_settings.py | 2 -- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index c160d8e062..1418bc210b 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -14,7 +14,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): # Execute after workfile template copy order = 10 app_groups = { - "3dsmax", + "3dsmax", "adsk_3dsmax", "maya", "nuke", "nukex", diff --git a/openpype/hosts/max/hooks/force_startup_script.py b/openpype/hosts/max/hooks/force_startup_script.py index d87697b819..5fb8334d4b 100644 --- a/openpype/hosts/max/hooks/force_startup_script.py +++ b/openpype/hosts/max/hooks/force_startup_script.py @@ -14,7 +14,7 @@ class ForceStartupScript(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ - app_groups = {"3dsmax"} + app_groups = {"3dsmax", "adsk_3dsmax"} order = 11 launch_types = {LaunchTypes.local} diff --git a/openpype/hosts/max/hooks/inject_python.py b/openpype/hosts/max/hooks/inject_python.py index 874884585e..e9dddbf710 100644 --- a/openpype/hosts/max/hooks/inject_python.py +++ b/openpype/hosts/max/hooks/inject_python.py @@ -13,7 +13,7 @@ class InjectPythonPath(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ - app_groups = {"3dsmax"} + app_groups = {"3dsmax", "adsk_3dsmax"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index cd12a8f757..904751e653 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -124,8 +124,6 @@ def _convert_applications_system_settings( # Applications settings ayon_apps = addon_settings["applications"] - if "adsk_3dsmax" in ayon_apps: - ayon_apps["3dsmax"] = ayon_apps.pop("adsk_3dsmax") additional_apps = ayon_apps.pop("additional_apps") applications = _convert_applications_groups( From f4f1484c6abc69dfa66f0abf4169da7e2b41f638 Mon Sep 17 00:00:00 2001 From: Mustafa Zarkash Date: Thu, 3 Aug 2023 22:57:26 +0300 Subject: [PATCH 441/446] Bugfix: update defaults to default_variants in maya and houdini OP DCC settings (#5407) * update defaults to default_variants * update defaults to defaults in Ayon dcc settings * increment maya and houdini ayon addons patch version --- .../defaults/project_settings/houdini.json | 2 +- .../defaults/project_settings/maya.json | 20 +++++++++---------- .../schemas/schema_houdini_create.json | 4 ++-- .../schemas/schema_maya_create.json | 20 +++++++++---------- .../server/settings/publish_plugins.py | 2 +- server_addon/houdini/server/version.py | 2 +- server_addon/maya/server/settings/creators.py | 20 +++++++++---------- server_addon/maya/server/version.py | 2 +- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index a53f1ff202..a5256aad8b 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -14,7 +14,7 @@ "create": { "CreateArnoldAss": { "enabled": true, - "defaults": [], + "default_variants": [], "ext": ".ass" }, "CreateAlembicCamera": { diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 8e1022f877..342d2bfb2a 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -521,7 +521,7 @@ "enabled": true, "make_tx": true, "rs_tex": false, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -533,7 +533,7 @@ }, "CreateUnrealStaticMesh": { "enabled": true, - "defaults": [ + "default_variants": [ "", "_Main" ], @@ -547,7 +547,7 @@ }, "CreateUnrealSkeletalMesh": { "enabled": true, - "defaults": [], + "default_variants": [], "joint_hints": "jnt_org" }, "CreateMultiverseLook": { @@ -559,7 +559,7 @@ "write_face_sets": false, "include_parent_hierarchy": false, "include_user_defined_attributes": false, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -567,7 +567,7 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, - "defaults": [ + "default_variants": [ "Main", "Proxy", "Sculpt" @@ -578,7 +578,7 @@ "write_color_sets": false, "write_face_sets": false, "include_user_defined_attributes": false, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -586,20 +586,20 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateReview": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ], "useMayaTimeline": true }, "CreateAss": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ], "expandProcedurals": false, @@ -621,7 +621,7 @@ "enabled": true, "vrmesh": true, "alembic": true, - "defaults": [ + "default_variants": [ "Main" ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index 83e0cf789a..64d157d281 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -18,7 +18,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" }, @@ -86,4 +86,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index d28d42c10c..8dec0a8817 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -28,7 +28,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -52,7 +52,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" }, @@ -84,7 +84,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" }, @@ -147,7 +147,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -177,7 +177,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -212,7 +212,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -242,7 +242,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -262,7 +262,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" }, @@ -287,7 +287,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" }, @@ -389,7 +389,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index ca5d0a4ea5..4155c75eb7 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -54,7 +54,7 @@ class CreatePluginsModel(BaseSettingsModel): DEFAULT_HOUDINI_CREATE_SETTINGS = { "CreateArnoldAss": { "enabled": True, - "defaults": [], + "default_variants": [], "ext": ".ass" }, "CreateAlembicCamera": { diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 291b3ec660..039b027898 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -224,7 +224,7 @@ DEFAULT_CREATORS_SETTINGS = { "enabled": True, "make_tx": True, "rs_tex": False, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -236,7 +236,7 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateUnrealStaticMesh": { "enabled": True, - "defaults": [ + "default_variants": [ "", "_Main" ], @@ -250,7 +250,7 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateUnrealSkeletalMesh": { "enabled": True, - "defaults": [], + "default_variants": [], "joint_hints": "jnt_org" }, "CreateMultiverseLook": { @@ -262,7 +262,7 @@ DEFAULT_CREATORS_SETTINGS = { "write_face_sets": False, "include_parent_hierarchy": False, "include_user_defined_attributes": False, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -270,7 +270,7 @@ DEFAULT_CREATORS_SETTINGS = { "enabled": True, "write_color_sets": False, "write_face_sets": False, - "defaults": [ + "default_variants": [ "Main", "Proxy", "Sculpt" @@ -281,7 +281,7 @@ DEFAULT_CREATORS_SETTINGS = { "write_color_sets": False, "write_face_sets": False, "include_user_defined_attributes": False, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -289,7 +289,7 @@ DEFAULT_CREATORS_SETTINGS = { "enabled": True, "write_color_sets": False, "write_face_sets": False, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -313,7 +313,7 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateAss": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ], "expandProcedurals": False, @@ -363,7 +363,7 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateReview": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ], "useMayaTimeline": True @@ -387,7 +387,7 @@ DEFAULT_CREATORS_SETTINGS = { "enabled": True, "vrmesh": True, "alembic": True, - "defaults": [ + "default_variants": [ "Main" ] }, diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index a242f0e757..df0c92f1e2 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.1" +__version__ = "0.1.2" From bdaf86700bc33f85f13ee0f9fb5897770cef1aba Mon Sep 17 00:00:00 2001 From: Mustafa Zarkash Date: Fri, 4 Aug 2023 15:28:33 +0300 Subject: [PATCH 442/446] Bugfix: houdini hard coded project settings (#5400) * get poject settings in creator * add comment about reading ext from project settings * update validator to get project settings * update comment about reading ext from project settings * revert explicit edits it's automated * remove redundant line --- openpype/hosts/houdini/api/plugin.py | 19 +++++++++++++++++++ .../plugins/create/create_arnold_ass.py | 2 ++ .../publish/validate_workfile_paths.py | 2 -- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 05e52e2478..70c837205e 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -167,6 +167,7 @@ class HoudiniCreatorBase(object): class HoudiniCreator(NewCreator, HoudiniCreatorBase): """Base class for most of the Houdini creator plugins.""" selected_nodes = [] + settings_name = None def create(self, subset_name, instance_data, pre_create_data): try: @@ -294,3 +295,21 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """ return [hou.ropNodeTypeCategory()] + + def apply_settings(self, project_settings, system_settings): + """Method called on initialization of plugin to apply settings.""" + + settings_name = self.settings_name + if settings_name is None: + settings_name = self.__class__.__name__ + + settings = project_settings["houdini"]["create"] + settings = settings.get(settings_name) + if settings is None: + self.log.debug( + "No settings found for {}".format(self.__class__.__name__) + ) + return + + for key, value in settings.items(): + setattr(self, key, value) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py index 8b310753d0..45ef9ea82f 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py @@ -13,6 +13,8 @@ class CreateArnoldAss(plugin.HoudiniCreator): defaults = ["Main"] # Default extension: `.ass` or `.ass.gz` + # however calling HoudiniCreator.create() + # will override it by the value in the project settings ext = ".ass" def create(self, subset_name, instance_data, pre_create_data): diff --git a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py index 543c8e1407..afe05e3173 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py @@ -7,8 +7,6 @@ from openpype.pipeline import ( ) from openpype.pipeline.publish import RepairAction -from openpype.pipeline.publish import RepairAction - class ValidateWorkfilePaths( pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): From 4ff85c7e1873e8ae9e71d6ec6245bf6e9aaca73c Mon Sep 17 00:00:00 2001 From: Jiri Sindelar Date: Fri, 4 Aug 2023 14:58:30 +0200 Subject: [PATCH 443/446] remove string conversion for instance name Should not be in this PR --- openpype/hosts/nuke/plugins/publish/extract_slate_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index 54c88717c5..06c086b10d 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -242,7 +242,7 @@ class ExtractSlateFrame(publish.Extractor): # render slate as sequence frame nuke.execute( - str(instance.data["name"]), + instance.data["name"], int(slate_first_frame), int(slate_first_frame) ) From 86f39e8e8f3cb7f6152982bae6486ba8125a03e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:58:50 +0200 Subject: [PATCH 444/446] Applications: Attributes creation (#5408) * merge applications and tools from all addon versions into one big set * bump applications version to '0.1.1' * impemented 'pre_setup' to fix old versions of applications addon * Fix version access --- server_addon/applications/server/__init__.py | 111 ++++++++++++++++--- server_addon/applications/server/version.py | 2 +- 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/server_addon/applications/server/__init__.py b/server_addon/applications/server/__init__.py index fdec05006b..e782e8a591 100644 --- a/server_addon/applications/server/__init__.py +++ b/server_addon/applications/server/__init__.py @@ -2,12 +2,68 @@ import os import json import copy -from ayon_server.addons import BaseServerAddon +from ayon_server.addons import BaseServerAddon, AddonLibrary from ayon_server.lib.postgres import Postgres from .version import __version__ from .settings import ApplicationsAddonSettings, DEFAULT_VALUES +try: + import semver +except ImportError: + semver = None + + +def sort_versions(addon_versions, reverse=False): + if semver is None: + for addon_version in sorted(addon_versions, reverse=reverse): + yield addon_version + return + + version_objs = [] + invalid_versions = [] + for addon_version in addon_versions: + try: + version_objs.append( + (addon_version, semver.VersionInfo.parse(addon_version)) + ) + except ValueError: + invalid_versions.append(addon_version) + + valid_versions = [ + addon_version + for addon_version, _ in sorted(version_objs, key=lambda x: x[1]) + ] + sorted_versions = list(sorted(invalid_versions)) + valid_versions + if reverse: + sorted_versions = reversed(sorted_versions) + for addon_version in sorted_versions: + yield addon_version + + +def merge_groups(output, new_groups): + groups_by_name = { + o_group["name"]: o_group + for o_group in output + } + extend_groups = [] + for new_group in new_groups: + group_name = new_group["name"] + if group_name not in groups_by_name: + extend_groups.append(new_group) + continue + existing_group = groups_by_name[group_name] + existing_variants = existing_group["variants"] + existing_variants_by_name = { + variant["name"]: variant + for variant in existing_variants + } + for new_variant in new_group["variants"]: + if new_variant["name"] not in existing_variants_by_name: + existing_variants.append(new_variant) + + output.extend(extend_groups) + def get_enum_items_from_groups(groups): label_by_name = {} @@ -22,12 +78,11 @@ def get_enum_items_from_groups(groups): full_name = f"{group_name}/{variant_name}" full_label = f"{group_label} {variant_label}" label_by_name[full_name] = full_label - enum_items = [] - for full_name in sorted(label_by_name): - enum_items.append( - {"value": full_name, "label": label_by_name[full_name]} - ) - return enum_items + + return [ + {"value": full_name, "label": label_by_name[full_name]} + for full_name in sorted(label_by_name) + ] class ApplicationsAddon(BaseServerAddon): @@ -48,6 +103,19 @@ class ApplicationsAddon(BaseServerAddon): return self.get_settings_model()(**default_values) + async def pre_setup(self): + """Make sure older version of addon use the new way of attributes.""" + + instance = AddonLibrary.getinstance() + app_defs = instance.data.get(self.name) + old_addon = app_defs.versions.get("0.1.0") + if old_addon is not None: + # Override 'create_applications_attribute' for older versions + # - avoid infinite server restart loop + old_addon.create_applications_attribute = ( + self.create_applications_attribute + ) + async def setup(self): need_restart = await self.create_applications_attribute() if need_restart: @@ -60,21 +128,32 @@ class ApplicationsAddon(BaseServerAddon): bool: 'True' if an attribute was created or updated. """ - settings_model = await self.get_studio_settings() - studio_settings = settings_model.dict() - applications = studio_settings["applications"] - _applications = applications.pop("additional_apps") - for name, value in applications.items(): - value["name"] = name - _applications.append(value) + instance = AddonLibrary.getinstance() + app_defs = instance.data.get(self.name) + all_applications = [] + all_tools = [] + for addon_version in sort_versions( + app_defs.versions.keys(), reverse=True + ): + addon = app_defs.versions[addon_version] + for variant in ("production", "staging"): + settings_model = await addon.get_studio_settings(variant) + studio_settings = settings_model.dict() + application_settings = studio_settings["applications"] + app_groups = application_settings.pop("additional_apps") + for group_name, value in application_settings.items(): + value["name"] = group_name + app_groups.append(value) + merge_groups(all_applications, app_groups) + merge_groups(all_tools, studio_settings["tool_groups"]) query = "SELECT name, position, scope, data from public.attributes" apps_attrib_name = "applications" tools_attrib_name = "tools" - apps_enum = get_enum_items_from_groups(_applications) - tools_enum = get_enum_items_from_groups(studio_settings["tool_groups"]) + apps_enum = get_enum_items_from_groups(all_applications) + tools_enum = get_enum_items_from_groups(all_tools) apps_attribute_data = { "type": "list_of_strings", "title": "Applications", diff --git a/server_addon/applications/server/version.py b/server_addon/applications/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/applications/server/version.py +++ b/server_addon/applications/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From 511b899397cf3d571e3a408c59c3a4464856112b Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 5 Aug 2023 03:24:43 +0000 Subject: [PATCH 445/446] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index bbe452aeba..12bff54676 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.3-nightly.3" +__version__ = "3.16.3-nightly.4" From 653a1bc0a83af2d82df9115a85a5d6f4155b937e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 5 Aug 2023 03:25:28 +0000 Subject: [PATCH 446/446] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b6a243bcfe..dea7e3c57f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.3-nightly.4 - 3.16.3-nightly.3 - 3.16.3-nightly.2 - 3.16.3-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.7-nightly.5 - 3.14.7-nightly.4 - 3.14.7-nightly.3 - - 3.14.7-nightly.2 validations: required: true - type: dropdown

^EF&ezGqpUan>Z)KMf77)A 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 Qki#1c@bo)U>mGjPzfEA$$N&HU literal 0 HcmV?d00001 diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py new file mode 100644 index 0000000000..0402a7b5f3 --- /dev/null +++ b/openpype/settings/ayon_settings.py @@ -0,0 +1,1141 @@ +"""Helper functionality to convert AYON settings to OpenPype v3 settings. + +The settings are converted, so we can use v3 code with AYON settings. Once +the code of and addon is converted to full AYON addon which expect AYON +settings the conversion function can be removed. + +The conversion is hardcoded -> there is no other way how to achieve the result. + +Main entrypoints are functions: +- convert_project_settings - convert settings to project settings +- convert_system_settings - convert settings to system settings +# Both getters cache values +- get_ayon_project_settings - replacement for 'get_project_settings' +- get_ayon_system_settings - replacement for 'get_system_settings' +""" + +import json +import copy +import time + +import six +import ayon_api + + +def _convert_color(color_value): + if isinstance(color_value, six.string_types): + color_value = color_value.lstrip("#") + color_value_len = len(color_value) + _color_value = [] + for idx in range(color_value_len // 2): + _color_value.append(int(color_value[idx:idx + 2], 16)) + for _ in range(4 - len(_color_value)): + _color_value.append(255) + return _color_value + + if isinstance(color_value, list): + # WARNING R,G,B can be 'int' or 'float' + # - 'float' variant is using 'int' for min: 0 and max: 1 + if len(color_value) == 3: + # Add alpha + color_value.append(255) + else: + # Convert float alha to int + alpha = int(color_value[3] * 255) + if alpha > 255: + alpha = 255 + elif alpha < 0: + alpha = 0 + color_value[3] = alpha + return color_value + + +def _convert_host_imageio(host_settings): + if "imageio" not in host_settings: + return + + # --- imageio --- + ayon_imageio = host_settings["imageio"] + # TODO remove when fixed on server + if "ocio_config" in ayon_imageio["ocio_config"]: + ayon_imageio["ocio_config"]["filepath"] = ( + ayon_imageio["ocio_config"].pop("ocio_config") + ) + # Convert file rules + imageio_file_rules = ayon_imageio["file_rules"] + new_rules = {} + for rule in imageio_file_rules["rules"]: + name = rule.pop("name") + new_rules[name] = rule + imageio_file_rules["rules"] = new_rules + + +def _convert_applications_groups(groups, clear_metadata): + environment_key = "environment" + if isinstance(groups, dict): + new_groups = [] + for name, item in groups.items(): + item["name"] = name + new_groups.append(item) + groups = new_groups + + output = {} + group_dynamic_labels = {} + for group in groups: + group_name = group.pop("name") + if "label" in group: + group_dynamic_labels[group_name] = group["label"] + + tool_group_envs = group[environment_key] + if isinstance(tool_group_envs, six.string_types): + group[environment_key] = json.loads(tool_group_envs) + + variants = {} + variant_dynamic_labels = {} + for variant in group.pop("variants"): + variant_name = variant.pop("name") + variant_dynamic_labels[variant_name] = variant.pop("label") + variant_envs = variant[environment_key] + if isinstance(variant_envs, six.string_types): + variant[environment_key] = json.loads(variant_envs) + variants[variant_name] = variant + group["variants"] = variants + + if not clear_metadata: + variants["__dynamic_keys_labels__"] = variant_dynamic_labels + output[group_name] = group + + if not clear_metadata: + output["__dynamic_keys_labels__"] = group_dynamic_labels + return output + + +def _convert_applications(ayon_settings, output, clear_metadata): + # Addon settings + addon_settings = ayon_settings["applications"] + + # Applications settings + ayon_apps = addon_settings["applications"] + additional_apps = ayon_apps.pop("additional_apps") + applications = _convert_applications_groups( + ayon_apps, clear_metadata + ) + applications["additional_apps"] = _convert_applications_groups( + additional_apps, clear_metadata + ) + + # Tools settings + tools = _convert_applications_groups( + addon_settings["tool_groups"], clear_metadata + ) + + output["applications"] = applications + output["tools"] = {"tool_groups": tools} + + +def _convert_general(ayon_settings, output): + # TODO get studio name/code + core_settings = ayon_settings["core"] + environments = core_settings["environments"] + if isinstance(environments, six.string_types): + environments = json.loads(environments) + + output["general"].update({ + "log_to_server": False, + "studio_name": core_settings["studio_name"], + "studio_code": core_settings["studio_code"], + "environments": environments + }) + + +def _convert_kitsu_system_settings(ayon_settings, output): + kitsu_settings = output["modules"]["kitsu"] + kitsu_settings["server"] = ayon_settings["kitsu"]["server"] + + +def _convert_ftrack_system_settings(ayon_settings, output): + # TODO implement and convert rest of ftrack settings + ftrack_settings = output["modules"]["ftrack"] + ayon_ftrack = ayon_settings["ftrack"] + ftrack_settings["ftrack_server"] = ayon_ftrack["ftrack_server"] + + +def _convert_shotgrid_system_settings(ayon_settings, output): + ayon_shotgrid = ayon_settings["shotgrid"] + # Skip conversion if different ayon addon is used + if "leecher_manager_url" not in ayon_shotgrid: + return + + shotgrid_settings = output["modules"]["shotgrid"] + for key in ( + "leecher_manager_url", + "leecher_backend_url", + "filter_projects_by_login", + ): + shotgrid_settings[key] = ayon_shotgrid[key] + + new_items = {} + for item in ayon_shotgrid["shotgrid_settings"]: + name = item.pop("name") + new_items[name] = item + shotgrid_settings["shotgrid_settings"] = new_items + + +def _convert_timers_manager(ayon_settings, output): + manager_settings = output["modules"]["timers_manager"] + ayon_manager = ayon_settings["timers_manager"] + for key in { + "auto_stop", "full_time", "message_time", "disregard_publishing" + }: + manager_settings[key] = ayon_manager[key] + + +def _convert_clockify(ayon_settings, output): + clockify_settings = output["modules"]["clockify"] + ayon_clockify = ayon_settings["clockify"] + for key in { + "worskpace_name", + }: + clockify_settings[key] = ayon_clockify[key] + + +def _convert_deadline(ayon_settings, output): + deadline_settings = output["modules"]["deadline"] + ayon_deadline = ayon_settings["deadline"] + deadline_urls = {} + for item in ayon_deadline["deadline_urls"]: + deadline_urls[item["name"]] = item["value"] + deadline_settings["deadline_urls"] = deadline_urls + + +def _convert_muster(ayon_settings, output): + muster_settings = output["modules"]["muster"] + ayon_muster = ayon_settings["muster"] + templates_mapping = {} + for item in ayon_muster["templates_mapping"]: + templates_mapping[item["name"]] = item["value"] + muster_settings["templates_mapping"] = templates_mapping + muster_settings["MUSTER_REST_URL"] = ayon_muster["MUSTER_REST_URL"] + + +def _convert_royalrender(ayon_settings, output): + royalrender_settings = output["modules"]["royalrender"] + ayon_royalrender = ayon_settings["royalrender"] + royalrender_settings["rr_paths"] = { + item["name"]: item["value"] + for item in ayon_royalrender["rr_paths"] + } + + +def _convert_modules(ayon_settings, output, addon_versions): + # TODO add all modules + # TODO add 'enabled' values + for key, func in ( + ("kitsu", _convert_kitsu_system_settings), + ("ftrack", _convert_ftrack_system_settings), + ("shotgrid", _convert_shotgrid_system_settings), + ("timers_manager", _convert_timers_manager), + ("clockify", _convert_clockify), + ("deadline", _convert_deadline), + ("muster", _convert_muster), + ("royalrender", _convert_royalrender), + ): + if key in ayon_settings: + func(ayon_settings, output) + + for module_name, value in output["modules"].items(): + if "enabled" not in value: + continue + value["enabled"] = module_name in addon_versions + + # Missing modules conversions + # - "sync_server" -> renamed to sitesync + # - "slack" -> only 'enabled' + # - "job_queue" -> completelly missing in ayon + + +def convert_system_settings(ayon_settings, default_settings, addon_versions): + output = copy.deepcopy(default_settings) + if "applications" in ayon_settings: + _convert_applications(ayon_settings, output, False) + + if "core" in ayon_settings: + _convert_general(ayon_settings, output) + + _convert_modules(ayon_settings, output, addon_versions) + return output + + +# --------- Project settings --------- +def _convert_blender_project_settings(ayon_settings, output): + if "blender" not in ayon_settings: + return + ayon_blender = ayon_settings["blender"] + blender_settings = output["blender"] + _convert_host_imageio(ayon_blender) + + ayon_workfile_build = ayon_blender["workfile_builder"] + blender_workfile_build = blender_settings["workfile_builder"] + for key in ("create_first_version", "custom_templates"): + blender_workfile_build[key] = ayon_workfile_build[key] + + ayon_publish = ayon_blender["publish"] + model_validators = ayon_publish.pop("model_validators", None) + if model_validators is not None: + for src_key, dst_key in ( + ("validate_mesh_has_uvs", "ValidateMeshHasUvs"), + ("validate_mesh_no_negative_scale", "ValidateMeshNoNegativeScale"), + ("validate_transform_zero", "ValidateTransformZero"), + ): + ayon_publish[dst_key] = model_validators.pop(src_key) + + blender_publish = blender_settings["publish"] + for key in tuple(ayon_publish.keys()): + blender_publish[key] = ayon_publish[key] + + +def _convert_celaction_project_settings(ayon_settings, output): + if "celaction" not in ayon_settings: + return + ayon_celaction_publish = ayon_settings["celaction"]["publish"] + celaction_publish_settings = output["celaction"]["publish"] + + output["celaction"]["imageio"] = _convert_host_imageio( + ayon_celaction_publish + ) + + for plugin_name in tuple(celaction_publish_settings.keys()): + if plugin_name in ayon_celaction_publish: + celaction_publish_settings[plugin_name] = ( + ayon_celaction_publish[plugin_name] + ) + + +def _convert_flame_project_settings(ayon_settings, output): + if "flame" not in ayon_settings: + return + + ayon_flame = ayon_settings["flame"] + flame_settings = output["flame"] + flame_settings["create"] = ayon_flame["create"] + + ayon_load_flame = ayon_flame["load"] + load_flame_settings = flame_settings["load"] + # Wrong settings model on server side + for src_key, dst_key in ( + ("load_clip", "LoadClip"), + ("load_clip_batch", "LoadClipBatch"), + ): + if src_key in ayon_load_flame: + ayon_load_flame[dst_key] = ayon_load_flame.pop(src_key) + + for plugin_name in tuple(load_flame_settings.keys()): + if plugin_name in ayon_load_flame: + load_flame_settings[plugin_name] = ayon_load_flame[plugin_name] + + ayon_publish_flame = ayon_flame["publish"] + flame_publish_settings = flame_settings["publish"] + # 'ExtractSubsetResources' changed model of 'export_presets_mapping' + # - some keys were moved under 'other_parameters' + ayon_subset_resources = ayon_publish_flame["ExtractSubsetResources"] + new_subset_resources = {} + for item in ayon_subset_resources.pop("export_presets_mapping"): + name = item.pop("name") + if "other_parameters" in item: + other_parameters = item.pop("other_parameters") + item.update(other_parameters) + new_subset_resources[name] = item + + ayon_subset_resources["export_presets_mapping"] = new_subset_resources + for plugin_name in tuple(flame_publish_settings.keys()): + if plugin_name in ayon_publish_flame: + flame_publish_settings[plugin_name] = ( + ayon_publish_flame[plugin_name] + ) + + # 'imageio' changed model + # - missing subkey 'project' which is in root of 'imageio' model + _convert_host_imageio(ayon_flame) + ayon_imageio_flame = ayon_flame["imageio"] + if "project" not in ayon_imageio_flame: + profile_mapping = ayon_imageio_flame.pop("profilesMapping") + ayon_imageio_flame = { + "project": ayon_imageio_flame, + "profilesMapping": profile_mapping + } + flame_settings["imageio"] = ayon_imageio_flame + + +def _convert_fusion_project_settings(ayon_settings, output): + if "fusion" not in ayon_settings: + return + ayon_fusion = ayon_settings["fusion"] + ayon_imageio_fusion = ayon_fusion["imageio"] + + if "ocioSettings" in ayon_imageio_fusion: + ayon_ocio_setting = ayon_imageio_fusion.pop("ocioSettings") + paths = ayon_ocio_setting.pop("ocioPathModel") + for key, value in tuple(paths.items()): + new_value = [] + if value: + new_value.append(value) + paths[key] = new_value + + ayon_ocio_setting["configFilePath"] = paths + ayon_imageio_fusion["ocio"] = ayon_ocio_setting + + _convert_host_imageio(ayon_imageio_fusion) + + imageio_fusion_settings = output["fusion"]["imageio"] + for key in ( + "imageio", + ): + imageio_fusion_settings[key] = ayon_fusion[key] + + +def _convert_maya_project_settings(ayon_settings, output): + if "maya" not in ayon_settings: + return + + ayon_maya = ayon_settings["maya"] + openpype_maya = output["maya"] + + # Change key of render settings + ayon_maya["RenderSettings"] = ayon_maya.pop("render_settings") + + # Convert extensions mapping + ayon_maya["ext_mapping"] = { + item["name"]: item["value"] + for item in ayon_maya["ext_mapping"] + } + + # Publish UI filters + new_filters = {} + for item in ayon_maya["filters"]: + new_filters[item["name"]] = { + subitem["name"]: subitem["value"] + for subitem in item["value"] + } + ayon_maya["filters"] = new_filters + + # Maya dirmap + ayon_maya_dirmap = ayon_maya.pop("maya_dirmap") + ayon_maya_dirmap_path = ayon_maya_dirmap["paths"] + ayon_maya_dirmap_path["source-path"] = ( + ayon_maya_dirmap_path.pop("source_path") + ) + ayon_maya_dirmap_path["destination-path"] = ( + ayon_maya_dirmap_path.pop("destination_path") + ) + ayon_maya["maya-dirmap"] = ayon_maya_dirmap + + # Create plugins + ayon_create = ayon_maya["create"] + ayon_create_static_mesh = ayon_create["CreateUnrealStaticMesh"] + if "static_mesh_prefixes" in ayon_create_static_mesh: + ayon_create_static_mesh["static_mesh_prefix"] = ( + ayon_create_static_mesh.pop("static_mesh_prefixes") + ) + + # --- Publish (START) --- + ayon_publish = ayon_maya["publish"] + try: + attributes = json.loads( + ayon_publish["ValidateAttributes"]["attributes"] + ) + except ValueError: + attributes = {} + ayon_publish["ValidateAttributes"]["attributes"] = attributes + + try: + SUFFIX_NAMING_TABLE = json.loads( + ayon_publish + ["ValidateTransformNamingSuffix"] + ["SUFFIX_NAMING_TABLE"] + ) + except ValueError: + SUFFIX_NAMING_TABLE = {} + ayon_publish["ValidateTransformNamingSuffix"]["SUFFIX_NAMING_TABLE"] = ( + SUFFIX_NAMING_TABLE + ) + + # Extract playblast capture settings + validate_rendern_settings = ayon_publish["ValidateRenderSettings"] + for key in ( + "arnold_render_attributes", + "vray_render_attributes", + "redshift_render_attributes", + "renderman_render_attributes", + ): + if key not in validate_rendern_settings: + continue + validate_rendern_settings[key] = [ + [item["type"], item["value"]] + for item in validate_rendern_settings[key] + ] + + ayon_capture_preset = ayon_publish["ExtractPlayblast"]["capture_preset"] + display_options = ayon_capture_preset["DisplayOptions"] + for key in ("background", "backgroundBottom", "backgroundTop"): + display_options[key] = _convert_color(display_options[key]) + + for src_key, dst_key in ( + ("DisplayOptions", "Display Options"), + ("ViewportOptions", "Viewport Options"), + ("CameraOptions", "Camera Options"), + ): + ayon_capture_preset[dst_key] = ayon_capture_preset.pop(src_key) + + # Extract Camera Alembic bake attributes + try: + bake_attributes = json.loads( + ayon_publish["ExtractCameraAlembic"]["bake_attributes"] + ) + except ValueError: + bake_attributes = [] + ayon_publish["ExtractCameraAlembic"]["bake_attributes"] = bake_attributes + + # --- Publish (END) --- + for renderer_settings in ayon_maya["RenderSettings"].values(): + if ( + not isinstance(renderer_settings, dict) + or "additional_options" not in renderer_settings + ): + continue + renderer_settings["additional_options"] = [ + [item["attribute"], item["value"]] + for item in renderer_settings["additional_options"] + ] + + _convert_host_imageio(ayon_maya) + + same_keys = { + "imageio", + "scriptsmenu", + "templated_workfile_build", + "load", + "create", + "publish", + "mel_workspace", + "ext_mapping", + "workfile_build", + "filters", + "maya-dirmap", + "RenderSettings", + } + for key in same_keys: + openpype_maya[key] = ayon_maya[key] + + +def _convert_nuke_knobs(knobs): + new_knobs = [] + for knob in knobs: + knob_type = knob["type"] + value = knob[knob_type] + + if knob_type == "boolean": + knob_type = "bool" + + new_knob = { + "type": knob_type, + "name": knob["name"], + } + new_knobs.append(new_knob) + + if knob_type == "formatable": + new_knob["template"] = value["template"] + new_knob["to_type"] = value["to_type"] + continue + + value_key = "value" + if knob_type == "expression": + value_key = "expression" + + elif knob_type == "color_gui": + value = _convert_color(value) + + elif knob_type == "vector_2d": + value = [value["x"], value["y"]] + + elif knob_type == "vector_3d": + value = [value["x"], value["y"], value["z"]] + + new_knob[value_key] = value + return new_knobs + + +def _convert_nuke_project_settings(ayon_settings, output): + if "nuke" not in ayon_settings: + return + + ayon_nuke = ayon_settings["nuke"] + openpype_nuke = output["nuke"] + + # --- Dirmap --- + dirmap = ayon_nuke.pop("dirmap") + for src_key, dst_key in ( + ("source_path", "source-path"), + ("destination_path", "destination-path"), + ): + dirmap["paths"][dst_key] = dirmap["paths"].pop(src_key) + ayon_nuke["nuke-dirmap"] = dirmap + + # --- Filters --- + new_gui_filters = {} + for item in ayon_nuke.pop("filters"): + subvalue = {} + key = item["name"] + for subitem in item["value"]: + subvalue[subitem["name"]] = subitem["value"] + new_gui_filters[key] = subvalue + ayon_nuke["filters"] = new_gui_filters + + # --- Load --- + ayon_load = ayon_nuke["load"] + ayon_load["LoadClip"]["_representations"] = ( + ayon_load["LoadClip"].pop("representations_include") + ) + ayon_load["LoadImage"]["_representations"] = ( + ayon_load["LoadImage"].pop("representations_include") + ) + + # --- Create --- + ayon_create = ayon_nuke["create"] + for creator_name in ( + "CreateWritePrerender", + "CreateWriteImage", + "CreateWriteRender", + ): + new_prenodes = {} + for prenode in ayon_create[creator_name]["prenodes"]: + name = prenode.pop("name") + prenode["knobs"] = _convert_nuke_knobs(prenode["knobs"]) + new_prenodes[name] = prenode + + ayon_create[creator_name]["prenodes"] = new_prenodes + + # --- Publish --- + ayon_publish = ayon_nuke["publish"] + slate_mapping = ayon_publish["ExtractSlateFrame"]["key_value_mapping"] + for key in tuple(slate_mapping.keys()): + value = slate_mapping[key] + slate_mapping[key] = [value["enabled"], value["template"]] + + ayon_publish["ValidateKnobs"]["knobs"] = json.loads( + ayon_publish["ValidateKnobs"]["knobs"] + ) + + new_review_data_outputs = {} + for item in ayon_publish["ExtractReviewDataMov"]["outputs"]: + name = item.pop("name") + item["reformat_node_config"] = _convert_nuke_knobs( + item["reformat_node_config"]) + new_review_data_outputs[name] = item + ayon_publish["ExtractReviewDataMov"]["outputs"] = new_review_data_outputs + + # TODO 'ExtractThumbnail' does not have ideal schema in v3 + new_thumbnail_nodes = {} + for item in ayon_publish["ExtractThumbnail"]["nodes"]: + name = item["nodeclass"] + value = [] + for knob in _convert_nuke_knobs(item["knobs"]): + knob_name = knob["name"] + # This may crash + if knob["type"] == "expression": + knob_value = knob["expression"] + else: + knob_value = knob["value"] + value.append([knob_name, knob_value]) + new_thumbnail_nodes[name] = value + + ayon_publish["ExtractThumbnail"]["nodes"] = new_thumbnail_nodes + + # --- ImageIO --- + # NOTE 'monitorOutLut' is maybe not yet in v3 (ut should be) + _convert_host_imageio(ayon_nuke) + ayon_imageio = ayon_nuke["imageio"] + for item in ayon_imageio["nodes"]["requiredNodes"]: + item["knobs"] = _convert_nuke_knobs(item["knobs"]) + for item in ayon_imageio["nodes"]["overrideNodes"]: + item["knobs"] = _convert_nuke_knobs(item["knobs"]) + + # Store converted values to openpype values + for key in ( + "scriptsmenu", + "nuke-dirmap", + "filters", + "load", + "create", + "publish", + "workfile_builder", + "imageio", + ): + openpype_nuke[key] = ayon_nuke[key] + + +def _convert_hiero_project_settings(ayon_settings, output): + if "hiero" not in ayon_settings: + return + + ayon_hiero = ayon_settings["hiero"] + openpype_hiero = output["hiero"] + + new_gui_filters = {} + for item in ayon_hiero.pop("filters"): + subvalue = {} + key = item["name"] + for subitem in item["value"]: + subvalue[subitem["name"]] = subitem["value"] + new_gui_filters[key] = subvalue + ayon_hiero["filters"] = new_gui_filters + + _convert_host_imageio(ayon_hiero) + + for key in ( + "create", + "filters", + "imageio", + "load", + "publish", + "scriptsmenu", + ): + openpype_hiero[key] = ayon_hiero[key] + + +def _convert_photoshop_project_settings(ayon_settings, output): + if "photoshop" not in ayon_settings: + return + + ayon_photoshop = ayon_settings["photoshop"] + photoshop_settings = output["photoshop"] + collect_review = ayon_photoshop["publish"]["CollectReview"] + if "active" in collect_review: + collect_review["publish"] = collect_review.pop("active") + + _convert_host_imageio(ayon_photoshop) + + for key in ( + "create", + "publish", + "workfile_builder", + "imageio", + ): + photoshop_settings[key] = ayon_photoshop[key] + + +def _convert_tvpaint_project_settings(ayon_settings, output): + if "tvpaint" not in ayon_settings: + return + ayon_tvpaint = ayon_settings["tvpaint"] + tvpaint_settings = output["tvpaint"] + + _convert_host_imageio(ayon_tvpaint) + + for key in ( + "stop_timer_on_application_exit", + "load", + "workfile_builder", + "imageio", + ): + tvpaint_settings[key] = ayon_tvpaint[key] + + filters = {} + for item in ayon_tvpaint["filters"]: + value = item["value"] + try: + value = json.loads(value) + + except ValueError: + value = {} + filters[item["name"]] = value + tvpaint_settings["filters"] = filters + + ayon_publish_settings = ayon_tvpaint["publish"] + tvpaint_publish_settings = tvpaint_settings["publish"] + for plugin_name in ("CollectRenderScene", "ExtractConvertToEXR"): + tvpaint_publish_settings[plugin_name] = ( + ayon_publish_settings[plugin_name] + ) + + for plugin_name in ( + "ValidateProjectSettings", + "ValidateMarks", + "ValidateStartFrame", + "ValidateAssetName", + ): + ayon_value = ayon_publish_settings[plugin_name] + tvpaint_value = tvpaint_publish_settings[plugin_name] + for src_key, dst_key in ( + ("action_enabled", "optional"), + ("action_enable", "active"), + ): + if src_key in ayon_value: + tvpaint_value[dst_key] = ayon_value[src_key] + + review_color = ayon_publish_settings["ExtractSequence"]["review_bg"] + tvpaint_publish_settings["ExtractSequence"]["review_bg"] = _convert_color( + review_color + ) + + +def _convert_traypublisher_project_settings(ayon_settings, output): + if "traypublisher" not in ayon_settings: + return + + ayon_traypublisher = ayon_settings["traypublisher"] + traypublisher_settings = output["traypublisher"] + + _convert_host_imageio(ayon_traypublisher) + traypublisher_settings["imageio"] = ayon_traypublisher["imageio"] + + ayon_editorial_simple = ( + ayon_traypublisher["editorial_creators"]["editorial_simple"] + ) + if "shot_metadata_creator" in ayon_editorial_simple: + shot_metadata_creator = ayon_editorial_simple.pop( + "shot_metadata_creator" + ) + if isinstance(shot_metadata_creator["clip_name_tokenizer"], dict): + shot_metadata_creator["clip_name_tokenizer"] = [ + {"name": "_sequence_", "regex": "(sc\\d{3})"}, + {"name": "_shot_", "regex": "(sh\\d{3})"}, + ] + ayon_editorial_simple.update(shot_metadata_creator) + + ayon_editorial_simple["clip_name_tokenizer"] = { + item["name"]: item["regex"] + for item in ayon_editorial_simple["clip_name_tokenizer"] + } + + if "shot_subset_creator" in ayon_editorial_simple: + ayon_editorial_simple.update( + ayon_editorial_simple.pop("shot_subset_creator")) + for item in ayon_editorial_simple["shot_hierarchy"]["parents"]: + item["type"] = item.pop("parent_type") + + shot_add_tasks = ayon_editorial_simple["shot_add_tasks"] + if isinstance(shot_add_tasks, dict): + shot_add_tasks = [] + new_shot_add_tasks = { + item["name"]: item["task_type"] + for item in shot_add_tasks + } + ayon_editorial_simple["shot_add_tasks"] = new_shot_add_tasks + + traypublisher_settings["editorial_creators"][ + "editorial_simple" + ] = ayon_editorial_simple + + +def _convert_webpublisher_project_settings(ayon_settings, output): + if "webpublisher" not in ayon_settings: + return + + ayon_webpublisher = ayon_settings["webpublisher"] + _convert_host_imageio(ayon_webpublisher) + + ayon_publish = ayon_webpublisher["publish"] + + ayon_collect_files = ayon_publish["CollectPublishedFiles"] + ayon_collect_files["task_type_to_family"] = { + item["name"]: item["value"] + for item in ayon_collect_files["task_type_to_family"] + } + output["webpublisher"]["publish"] = ayon_publish + output["webpublisher"]["imageio"] = ayon_webpublisher["imageio"] + + +def _convert_deadline_project_settings(ayon_settings, output): + if "deadline" not in ayon_settings: + return + + ayon_deadline = ayon_settings["deadline"] + deadline_settings = output["deadline"] + + for key in ("deadline_urls",): + ayon_deadline.pop(key) + + ayon_deadline_publish = ayon_deadline["publish"] + limit_groups = { + item["name"]: item["value"] + for item in ayon_deadline_publish["NukeSubmitDeadline"]["limit_groups"] + } + ayon_deadline_publish["NukeSubmitDeadline"]["limit_groups"] = limit_groups + + maya_submit = ayon_deadline_publish["MayaSubmitDeadline"] + for json_key in ("jobInfo", "pluginInfo"): + src_text = maya_submit.pop(json_key) + try: + value = json.loads(src_text) + except ValueError: + value = {} + maya_submit[json_key] = value + + nuke_submit = ayon_deadline_publish["NukeSubmitDeadline"] + nuke_submit["env_search_replace_values"] = { + item["name"]: item["value"] + for item in nuke_submit.pop("env_search_replace_values") + } + nuke_submit["limit_groups"] = { + item["name"]: item["value"] for item in nuke_submit.pop("limit_groups") + } + + process_subsetted_job = ayon_deadline_publish["ProcessSubmittedJobOnFarm"] + process_subsetted_job["aov_filter"] = { + item["name"]: item["value"] + for item in process_subsetted_job.pop("aov_filter") + } + deadline_publish_settings = deadline_settings["publish"] + for key in tuple(deadline_publish_settings.keys()): + if key in ayon_deadline_publish: + deadline_publish_settings[key] = ayon_deadline_publish[key] + + +def _convert_kitsu_project_settings(ayon_settings, output): + if "kitsu" not in ayon_settings: + return + + ayon_kitsu = ayon_settings["kitsu"] + kitsu_settings = output["kitsu"] + for key in tuple(kitsu_settings.keys()): + if key in ayon_kitsu: + kitsu_settings[key] = ayon_kitsu[key] + + +def _convert_shotgrid_project_settings(ayon_settings, output): + if "shotgrid" not in ayon_settings: + return + + ayon_shotgrid = ayon_settings["shotgrid"] + for key in { + "leecher_backend_url", + "filter_projects_by_login", + "shotgrid_settings", + "leecher_manager_url", + }: + ayon_shotgrid.pop(key) + + asset_field = ayon_shotgrid["fields"]["asset"] + asset_field["type"] = asset_field.pop("asset_type") + + task_field = ayon_shotgrid["fields"]["task"] + if "task" in task_field: + task_field["step"] = task_field.pop("task") + + shotgrid_settings = output["shotgrid"] + for key in tuple(shotgrid_settings.keys()): + if key in ayon_shotgrid: + shotgrid_settings[key] = ayon_shotgrid[key] + + +def _convert_slack_project_settings(ayon_settings, output): + if "slack" not in ayon_settings: + return + + ayon_slack = ayon_settings["slack"] + slack_settings = output["slack"] + ayon_slack.pop("enabled", None) + for profile in ayon_slack["publish"]["CollectSlackFamilies"]["profiles"]: + profile["tasks"] = profile.pop("task_names") + profile["subsets"] = profile.pop("subset_names") + + for key in tuple(slack_settings.keys()): + if key in ayon_settings: + slack_settings[key] = ayon_settings[key] + + +def _convert_global_project_settings(ayon_settings, output): + if "core" not in ayon_settings: + return + + ayon_core = ayon_settings["core"] + global_settings = output["global"] + + # Publish conversion + ayon_publish = ayon_core["publish"] + for profile in ayon_publish["ExtractReview"]["profiles"]: + new_outputs = {} + for output_def in profile.pop("outputs"): + name = output_def.pop("name") + new_outputs[name] = output_def + + for color_key in ("overscan_color", "bg_color"): + output_def[color_key] = _convert_color(output_def[color_key]) + + letter_box = output_def["letter_box"] + for color_key in ("fill_color", "line_color"): + letter_box[color_key] = _convert_color(letter_box[color_key]) + + if "output_width" in output_def: + output_def["width"] = output_def.pop("output_width") + + if "output_height" in output_def: + output_def["height"] = output_def.pop("output_height") + + profile["outputs"] = new_outputs + + extract_burnin = ayon_publish["ExtractBurnin"] + extract_burnin_options = extract_burnin["options"] + for color_key in ("font_color", "bg_color"): + extract_burnin_options[color_key] = _convert_color( + extract_burnin_options[color_key] + ) + + for profile in extract_burnin["profiles"]: + extract_burnin_defs = profile["burnins"] + profile["burnins"] = { + extract_burnin_def.pop("name"): extract_burnin_def + for extract_burnin_def in extract_burnin_defs + } + + global_publish = global_settings["publish"] + ayon_integrate_hero = ayon_publish["IntegrateHeroVersion"] + global_integrate_hero = global_publish["IntegrateHeroVersion"] + for key, value in global_integrate_hero.items(): + if key not in ayon_integrate_hero: + ayon_integrate_hero[key] = value + + ayon_cleanup = ayon_publish["CleanUp"] + if "patterns" in ayon_cleanup: + ayon_cleanup["paterns"] = ayon_cleanup.pop("patterns") + + for key in tuple(global_publish.keys()): + if key in ayon_publish: + global_publish[key] = ayon_publish[key] + + # Project root settings + for json_key in ("project_folder_structure", "project_environments"): + try: + value = json.loads(ayon_core[json_key]) + except ValueError: + value = {} + global_publish[json_key] = value + + # Tools settings + ayon_tools = ayon_core["tools"] + global_tools = global_settings["tools"] + ayon_create_tool = ayon_tools["creator"] + new_smart_select_families = { + item["name"]: item["task_names"] + for item in ayon_create_tool["families_smart_select"] + } + ayon_create_tool["families_smart_select"] = new_smart_select_families + global_tools["creator"] = ayon_create_tool + + ayon_loader_tool = ayon_tools["loader"] + for profile in ayon_loader_tool["family_filter_profiles"]: + if "template_publish_families" in profile: + profile["filter_families"] = ( + profile.pop("template_publish_families") + ) + global_tools["loader"] = ayon_loader_tool + + global_tools["publish"] = ayon_tools["publish"] + + +def convert_project_settings(ayon_settings, default_settings): + # Missing settings + # - standalonepublisher + output = copy.deepcopy(default_settings) + exact_match = { + "aftereffects", + "harmony", + "houdini", + "resolve", + "unreal", + } + for key in exact_match: + if key in ayon_settings: + output[key] = ayon_settings[key] + + _convert_blender_project_settings(ayon_settings, output) + _convert_celaction_project_settings(ayon_settings, output) + _convert_flame_project_settings(ayon_settings, output) + _convert_fusion_project_settings(ayon_settings, output) + _convert_maya_project_settings(ayon_settings, output) + _convert_nuke_project_settings(ayon_settings, output) + _convert_hiero_project_settings(ayon_settings, output) + _convert_photoshop_project_settings(ayon_settings, output) + _convert_tvpaint_project_settings(ayon_settings, output) + _convert_traypublisher_project_settings(ayon_settings, output) + _convert_webpublisher_project_settings(ayon_settings, output) + + _convert_deadline_project_settings(ayon_settings, output) + _convert_kitsu_project_settings(ayon_settings, output) + _convert_shotgrid_project_settings(ayon_settings, output) + _convert_slack_project_settings(ayon_settings, output) + + _convert_global_project_settings(ayon_settings, output) + + return output + + +class CacheItem: + lifetime = 10 + + def __init__(self, value): + self._value = value + self._outdate_time = time.time() + self.lifetime + + def get_value(self): + return copy.deepcopy(self._value) + + def update_value(self, value): + self._value = value + self._outdate_time = time.time() + self.lifetime + + @property + def is_outdated(self): + return time.time() > self._outdate_time + + +class AyonSettingsCache: + _cache_by_project_name = {} + _production_settings = None + + @classmethod + def get_production_settings(cls): + if ( + cls._production_settings is None + or cls._production_settings.is_outdated + ): + value = ayon_api.get_addons_settings(only_values=False) + if cls._production_settings is None: + cls._production_settings = CacheItem(value) + else: + cls._production_settings.update_value(value) + return cls._production_settings.get_value() + + @classmethod + def get_value_by_project(cls, project_name): + production_settings = cls.get_production_settings() + addon_versions = production_settings["versions"] + if project_name is None: + return production_settings["settings"], addon_versions + + cache_item = cls._cache_by_project_name.get(project_name) + if cache_item is None or cache_item.is_outdated: + value = ayon_api.get_addons_settings(project_name) + if cache_item is None: + cache_item = CacheItem(value) + cls._cache_by_project_name[project_name] = cache_item + else: + cache_item.update_value(value) + + return cache_item.get_value(), addon_versions + + +def get_ayon_project_settings(default_values, project_name): + ayon_settings, addon_versions = ( + AyonSettingsCache.get_value_by_project(project_name) + ) + return convert_project_settings(ayon_settings, default_values) + + +def get_ayon_system_settings(default_values): + ayon_settings, addon_versions = ( + AyonSettingsCache.get_value_by_project(None) + ) + return convert_system_settings( + ayon_settings, default_values, addon_versions + ) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index a1f3331ccc..ab7cdd058c 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -7,10 +7,14 @@ from abc import ABCMeta, abstractmethod import six import openpype.version -from openpype.client.mongo import OpenPypeMongoConnection -from openpype.client.entities import get_project_connection, get_project +from openpype.client.mongo import ( + OpenPypeMongoConnection, + get_project_connection, +) +from openpype.client.entities import get_project from openpype.lib.pype_info import get_workstation_info + from .constants import ( GLOBAL_SETTINGS_KEY, SYSTEM_SETTINGS_KEY, diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 73554df236..ce62dde43f 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -4,6 +4,9 @@ import functools import logging import platform import copy + +from openpype import AYON_SERVER_ENABLED + from .exceptions import ( SaveWarningExc ) @@ -18,6 +21,11 @@ from .constants import ( DEFAULT_PROJECT_KEY ) +from .ayon_settings import ( + get_ayon_project_settings, + get_ayon_system_settings +) + log = logging.getLogger(__name__) # Py2 + Py3 json decode exception @@ -40,36 +48,17 @@ _SETTINGS_HANDLER = None _LOCAL_SETTINGS_HANDLER = None -def require_handler(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - global _SETTINGS_HANDLER - if _SETTINGS_HANDLER is None: - _SETTINGS_HANDLER = create_settings_handler() - return func(*args, **kwargs) - return wrapper - - -def require_local_handler(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - global _LOCAL_SETTINGS_HANDLER - if _LOCAL_SETTINGS_HANDLER is None: - _LOCAL_SETTINGS_HANDLER = create_local_settings_handler() - return func(*args, **kwargs) - return wrapper - - -def create_settings_handler(): - from .handlers import MongoSettingsHandler - # Handler can't be created in global space on initialization but only when - # needed. Plus here may be logic: Which handler is used (in future). - return MongoSettingsHandler() - - -def create_local_settings_handler(): - from .handlers import MongoLocalSettingsHandler - return MongoLocalSettingsHandler() +def clear_metadata_from_settings(values): + """Remove all metadata keys from loaded settings.""" + if isinstance(values, dict): + for key in tuple(values.keys()): + if key in METADATA_KEYS: + values.pop(key) + else: + clear_metadata_from_settings(values[key]) + elif isinstance(values, list): + for item in values: + clear_metadata_from_settings(item) def calculate_changes(old_value, new_value): @@ -91,6 +80,42 @@ def calculate_changes(old_value, new_value): return changes +def create_settings_handler(): + if AYON_SERVER_ENABLED: + raise RuntimeError("Mongo settings handler was triggered in AYON mode") + from .handlers import MongoSettingsHandler + # Handler can't be created in global space on initialization but only when + # needed. Plus here may be logic: Which handler is used (in future). + return MongoSettingsHandler() + + +def create_local_settings_handler(): + if AYON_SERVER_ENABLED: + raise RuntimeError("Mongo settings handler was triggered in AYON mode") + from .handlers import MongoLocalSettingsHandler + return MongoLocalSettingsHandler() + + +def require_handler(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + global _SETTINGS_HANDLER + if _SETTINGS_HANDLER is None: + _SETTINGS_HANDLER = create_settings_handler() + return func(*args, **kwargs) + return wrapper + + +def require_local_handler(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + global _LOCAL_SETTINGS_HANDLER + if _LOCAL_SETTINGS_HANDLER is None: + _LOCAL_SETTINGS_HANDLER = create_local_settings_handler() + return func(*args, **kwargs) + return wrapper + + @require_handler def get_system_last_saved_info(): return _SETTINGS_HANDLER.get_system_last_saved_info() @@ -494,10 +519,17 @@ def save_local_settings(data): @require_local_handler -def get_local_settings(): +def _get_local_settings(): return _LOCAL_SETTINGS_HANDLER.get_local_settings() +def get_local_settings(): + if not AYON_SERVER_ENABLED: + return _get_local_settings() + # TODO implement ayon implementation + return {} + + def load_openpype_default_settings(): """Load openpype default settings.""" return load_jsons_from_dir(DEFAULTS_DIR) @@ -890,7 +922,7 @@ def apply_local_settings_on_project_settings( sync_server_config["remote_site"] = remote_site -def get_system_settings(clear_metadata=True, exclude_locals=None): +def _get_system_settings(clear_metadata=True, exclude_locals=None): """System settings with applied studio overrides.""" default_values = get_default_settings()[SYSTEM_SETTINGS_KEY] studio_values = get_studio_system_settings_overrides() @@ -992,7 +1024,7 @@ def get_anatomy_settings( return result -def get_project_settings( +def _get_project_settings( project_name, clear_metadata=True, exclude_locals=None ): """Project settings with applied studio and project overrides.""" @@ -1043,7 +1075,7 @@ def get_current_project_settings(): @require_handler -def get_global_settings(): +def _get_global_settings(): default_settings = load_openpype_default_settings() default_values = default_settings["system_settings"]["general"] studio_values = _SETTINGS_HANDLER.get_global_settings() @@ -1053,7 +1085,14 @@ def get_global_settings(): } -def get_general_environments(): +def get_global_settings(): + if not AYON_SERVER_ENABLED: + return _get_global_settings() + default_settings = load_openpype_default_settings() + return default_settings["system_settings"]["general"] + + +def _get_general_environments(): """Get general environments. Function is implemented to be able load general environments without using @@ -1082,14 +1121,24 @@ def get_general_environments(): return environments -def clear_metadata_from_settings(values): - """Remove all metadata keys from loaded settings.""" - if isinstance(values, dict): - for key in tuple(values.keys()): - if key in METADATA_KEYS: - values.pop(key) - else: - clear_metadata_from_settings(values[key]) - elif isinstance(values, list): - for item in values: - clear_metadata_from_settings(item) +def get_general_environments(): + if not AYON_SERVER_ENABLED: + return _get_general_environments() + value = get_system_settings() + return value["general"]["environment"] + + +def get_system_settings(*args, **kwargs): + if not AYON_SERVER_ENABLED: + return _get_system_settings(*args, **kwargs) + + default_settings = get_default_settings()[SYSTEM_SETTINGS_KEY] + return get_ayon_system_settings(default_settings) + + +def get_project_settings(project_name, *args, **kwargs): + if not AYON_SERVER_ENABLED: + return _get_project_settings(project_name, *args, **kwargs) + + default_settings = get_default_settings()[PROJECT_SETTINGS_KEY] + return get_ayon_project_settings(default_settings, project_name) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index e58e02f89a..1ec695b915 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -1173,9 +1173,9 @@ class RepresentationModel(TreeModel, BaseRepresentationModel): repre_groups_items[doc["name"]] = 0 group = group_item - progress = lib.get_progress_for_repre( - doc, self.active_site, self.remote_site - ) + progress = self.sync_server.get_progress_for_repre( + doc, + self.active_site, self.remote_site) active_site_icon = self._icons.get(self.active_provider) remote_site_icon = self._icons.get(self.remote_provider) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index b3aa381d14..5dd3af08d6 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -886,7 +886,9 @@ class ThumbnailWidget(QtWidgets.QLabel): self.set_pixmap() return - thumbnail_ent = get_thumbnail(project_name, thumbnail_id) + thumbnail_ent = get_thumbnail( + project_name, thumbnail_id, src_type, src_id + ) if not thumbnail_ent: return diff --git a/openpype/tools/sceneinventory/lib.py b/openpype/tools/sceneinventory/lib.py index 5db3c479c5..4b1860342a 100644 --- a/openpype/tools/sceneinventory/lib.py +++ b/openpype/tools/sceneinventory/lib.py @@ -28,55 +28,3 @@ def get_site_icons(): return icons - -def get_progress_for_repre(repre_doc, active_site, remote_site): - """ - Calculates average progress for representation. - - If site has created_dt >> fully available >> progress == 1 - - Could be calculated in aggregate if it would be too slow - Args: - repre_doc(dict): representation dict - Returns: - (dict) with active and remote sites progress - {'studio': 1.0, 'gdrive': -1} - gdrive site is not present - -1 is used to highlight the site should be added - {'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not - uploaded yet - """ - progress = {active_site: -1, remote_site: -1} - if not repre_doc: - return progress - - files = {active_site: 0, remote_site: 0} - doc_files = repre_doc.get("files") or [] - for doc_file in doc_files: - if not isinstance(doc_file, dict): - continue - - sites = doc_file.get("sites") or [] - for site in sites: - if ( - # Pype 2 compatibility - not isinstance(site, dict) - # Check if site name is one of progress sites - or site["name"] not in progress - ): - continue - - files[site["name"]] += 1 - norm_progress = max(progress[site["name"]], 0) - if site.get("created_dt"): - progress[site["name"]] = norm_progress + 1 - elif site.get("progress"): - progress[site["name"]] = norm_progress + site["progress"] - else: # site exists, might be failed, do not add again - progress[site["name"]] = 0 - - # for example 13 fully avail. files out of 26 >> 13/26 = 0.5 - avg_progress = { - active_site: progress[active_site] / max(files[active_site], 1), - remote_site: progress[remote_site] / max(files[remote_site], 1) - } - return avg_progress diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 5cc849bb9e..815ecf9efe 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -27,7 +27,6 @@ from openpype.modules import ModulesManager from .lib import ( get_site_icons, walk_hierarchy, - get_progress_for_repre ) @@ -80,7 +79,7 @@ class InventoryModel(TreeModel): project_name, remote_site ) - # self.sync_server = sync_server + self.sync_server = sync_server self.active_site = active_site self.active_provider = active_provider self.remote_site = remote_site @@ -445,7 +444,7 @@ class InventoryModel(TreeModel): group_node["group"] = subset["data"].get("subsetGroup") if self.sync_enabled: - progress = get_progress_for_repre( + progress = self.sync_server.get_progress_for_repre( representation, self.active_site, self.remote_site ) group_node["active_site"] = self.active_site diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 57e6e24411..d22b2bdd0f 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -23,7 +23,6 @@ from openpype.pipeline import ( ) from openpype.modules import ModulesManager from openpype.tools.utils.lib import ( - get_progress_for_repre, iter_model_rows, format_version ) @@ -361,7 +360,7 @@ class SceneInventoryView(QtWidgets.QTreeView): if not repre_doc: continue - progress = get_progress_for_repre( + progress = self.sync_server.get_progress_for_repre( repre_doc, active_site, remote_site diff --git a/openpype/tools/tray/pype_info_widget.py b/openpype/tools/tray/pype_info_widget.py index c616ad4dba..dc222b79b5 100644 --- a/openpype/tools/tray/pype_info_widget.py +++ b/openpype/tools/tray/pype_info_widget.py @@ -2,11 +2,14 @@ import os import json import collections +import ayon_api from qtpy import QtCore, QtGui, QtWidgets from openpype import style from openpype import resources +from openpype import AYON_SERVER_ENABLED from openpype.settings.lib import get_local_settings +from openpype.lib import get_openpype_execute_args from openpype.lib.pype_info import ( get_all_current_info, get_openpype_info, @@ -327,8 +330,9 @@ class PypeInfoSubWidget(QtWidgets.QWidget): main_layout.addWidget(self._create_openpype_info_widget(), 0) main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_workstation_widget(), 0) - main_layout.addWidget(self._create_separator(), 0) - main_layout.addWidget(self._create_local_settings_widget(), 0) + if not AYON_SERVER_ENABLED: + main_layout.addWidget(self._create_separator(), 0) + main_layout.addWidget(self._create_local_settings_widget(), 0) main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_environ_widget(), 1) @@ -425,31 +429,59 @@ class PypeInfoSubWidget(QtWidgets.QWidget): def _create_openpype_info_widget(self): """Create widget with information about OpenPype application.""" - # Get pype info data - pype_info = get_openpype_info() - # Modify version key/values - version_value = "{} ({})".format( - pype_info.pop("version", self.not_applicable), - pype_info.pop("version_type", self.not_applicable) - ) - pype_info["version_value"] = version_value - # Prepare label mapping - key_label_mapping = { - "version_value": "Running version:", - "build_verison": "Build version:", - "executable": "OpenPype executable:", - "pype_root": "OpenPype location:", - "mongo_url": "OpenPype Mongo URL:" - } - # Prepare keys order - keys_order = [ - "version_value", - "build_verison", - "executable", - "pype_root", - "mongo_url" - ] - for key in pype_info.keys(): + if AYON_SERVER_ENABLED: + executable_args = get_openpype_execute_args() + username = "N/A" + user_info = ayon_api.get_user() + if user_info: + username = user_info.get("name") or username + full_name = user_info.get("attrib", {}).get("fullName") + if full_name: + username = "{} ({})".format(full_name, username) + info_values = { + "executable": executable_args[-1], + "server_url": os.environ["AYON_SERVER_URL"], + "username": username + } + key_label_mapping = { + "executable": "AYON Executable:", + "server_url": "AYON Server:", + "username": "AYON Username:" + } + # Prepare keys order + keys_order = [ + "server_url", + "username", + "executable", + ] + + else: + # Get pype info data + info_values = get_openpype_info() + # Modify version key/values + version_value = "{} ({})".format( + info_values.pop("version", self.not_applicable), + info_values.pop("version_type", self.not_applicable) + ) + info_values["version_value"] = version_value + # Prepare label mapping + key_label_mapping = { + "version_value": "Running version:", + "build_verison": "Build version:", + "executable": "OpenPype executable:", + "pype_root": "OpenPype location:", + "mongo_url": "OpenPype Mongo URL:" + } + # Prepare keys order + keys_order = [ + "version_value", + "build_verison", + "executable", + "pype_root", + "mongo_url" + ] + + for key in info_values.keys(): if key not in keys_order: keys_order.append(key) @@ -466,9 +498,9 @@ class PypeInfoSubWidget(QtWidgets.QWidget): info_layout.addWidget(title_label, 0, 0, 1, 2) for key in keys_order: - if key not in pype_info: + if key not in info_values: continue - value = pype_info[key] + value = info_values[key] label = key_label_mapping.get(key, key) row = info_layout.rowCount() info_layout.addWidget( diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index fdc0a8094d..1cf128e59d 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -8,6 +8,7 @@ import platform from qtpy import QtCore, QtGui, QtWidgets import openpype.version +from openpype import AYON_SERVER_ENABLED from openpype import resources, style from openpype.lib import ( Logger, @@ -589,6 +590,11 @@ class TrayManager: self.tray_widget.showMessage(*args, **kwargs) def _add_version_item(self): + if AYON_SERVER_ENABLED: + login_action = QtWidgets.QAction("Login", self.tray_widget) + login_action.triggered.connect(self._on_ayon_login) + self.tray_widget.menu.addAction(login_action) + subversion = os.environ.get("OPENPYPE_SUBVERSION") client_name = os.environ.get("OPENPYPE_CLIENT") @@ -614,6 +620,19 @@ class TrayManager: self._restart_action = restart_action + def _on_ayon_login(self): + self.execute_in_main_thread(self._show_ayon_login) + + def _show_ayon_login(self): + from ayon_common.connection.credentials import change_user_ui + + result = change_user_ui() + if result.shutdown: + self.exit() + + elif result.restart or result.token_changed: + self.restart() + def _on_restart_action(self): self.restart(use_expected_version=True) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 58ece7c68f..7b3faddf08 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -752,61 +752,6 @@ def get_repre_icons(): return icons -def get_progress_for_repre(doc, active_site, remote_site): - """ - Calculates average progress for representation. - - If site has created_dt >> fully available >> progress == 1 - - Could be calculated in aggregate if it would be too slow - Args: - doc(dict): representation dict - Returns: - (dict) with active and remote sites progress - {'studio': 1.0, 'gdrive': -1} - gdrive site is not present - -1 is used to highlight the site should be added - {'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not - uploaded yet - """ - progress = {active_site: -1, - remote_site: -1} - if not doc: - return progress - - files = {active_site: 0, remote_site: 0} - doc_files = doc.get("files") or [] - for doc_file in doc_files: - if not isinstance(doc_file, dict): - continue - - sites = doc_file.get("sites") or [] - for site in sites: - if ( - # Pype 2 compatibility - not isinstance(site, dict) - # Check if site name is one of progress sites - or site["name"] not in progress - ): - continue - - files[site["name"]] += 1 - norm_progress = max(progress[site["name"]], 0) - if site.get("created_dt"): - progress[site["name"]] = norm_progress + 1 - elif site.get("progress"): - progress[site["name"]] = norm_progress + site["progress"] - else: # site exists, might be failed, do not add again - progress[site["name"]] = 0 - - # for example 13 fully avail. files out of 26 >> 13/26 = 0.5 - avg_progress = {} - avg_progress[active_site] = \ - progress[active_site] / max(files[active_site], 1) - avg_progress[remote_site] = \ - progress[remote_site] / max(files[remote_site], 1) - return avg_progress - - def is_sync_loader(loader): return is_remove_site_loader(loader) or is_add_site_loader(loader) diff --git a/poetry.lock b/poetry.lock index d7bdc5f7c4..f915832fb8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -302,6 +302,24 @@ files = [ pycodestyle = ">=2.10.0" tomli = {version = "*", markers = "python_version < \"3.11\""} +[[package]] +name = "ayon-python-api" +version = "0.1.16" +description = "AYON Python API" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "ayon-python-api-0.1.16.tar.gz", hash = "sha256:666110954dd75b2be1699a29b4732cfb0bcb09d01f64fba4449bfc8ac1fb43f1"}, + {file = "ayon_python_api-0.1.16-py3-none-any.whl", hash = "sha256:bbcd6df1f80ddf32e653a1bb31289cb5fd1a8bea36ab4c8e6aef08c41b6393de"}, +] + +[package.dependencies] +appdirs = ">=1,<2" +requests = ">=2.27.1" +six = ">=1.15" +Unidecode = ">=1.2.0" + [[package]] name = "babel" version = "2.11.0" @@ -3371,14 +3389,14 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "unidecode" -version = "1.3.6" +version = "1.2.0" description = "ASCII transliterations of Unicode text" -category = "dev" -optional = true -python-versions = ">=3.5" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, - {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, + {file = "Unidecode-1.2.0-py2.py3-none-any.whl", hash = "sha256:12435ef2fc4cdfd9cf1035a1db7e98b6b047fe591892e81f34e94959591fad00"}, + {file = "Unidecode-1.2.0.tar.gz", hash = "sha256:8d73a97d387a956922344f6b74243c2c6771594659778744b2dbdaad8f6b727d"}, ] [[package]] @@ -3672,10 +3690,7 @@ files = [ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] -[extras] -docs = [] - [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "47518c544a90cdb3e99e83533557515d0d47079ac4461708ce71ab3ce97b9987" +content-hash = "6bdb0572a9e255898497ad5ec4d7368d4e0850ce9f4d5c72a37394a2f8f7ec06" diff --git a/pyproject.toml b/pyproject.toml index fe9c228ea9..ebd7ea127d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,9 @@ requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" +ayon-python-api = "^0.1" opencolorio = "^2.2.0" +Unidecode = "^1.2" [tool.poetry.dev-dependencies] flake8 = "^6.0" diff --git a/setup.py b/setup.py index ab6e22bccc..f915f0e8ae 100644 --- a/setup.py +++ b/setup.py @@ -126,6 +126,7 @@ bin_includes = [ include_files = [ "igniter", "openpype", + "common", "schema", "LICENSE", "README.md" @@ -158,11 +159,35 @@ bdist_mac_options = dict( ) executables = [ - Executable("start.py", base=base, - target_name="openpype_gui", icon=icon_path.as_posix()), - Executable("start.py", base=None, - target_name="openpype_console", icon=icon_path.as_posix()) + Executable( + "start.py", + base=base, + target_name="openpype_gui", + icon=icon_path.as_posix() + ), + Executable( + "start.py", + base=None, + target_name="openpype_console", + icon=icon_path.as_posix() + ), + Executable( + "ayon_start.py", + base=base, + target_name="ayon", + icon=icon_path.as_posix() + ), ] +if IS_WINDOWS: + executables.append( + Executable( + "ayon_start.py", + base=None, + target_name="ayon_console", + icon=icon_path.as_posix() + ) + ) + if IS_LINUX: executables.append( Executable( diff --git a/start.py b/start.py index 36e2540200..f8d65dc221 100644 --- a/start.py +++ b/start.py @@ -133,6 +133,10 @@ else: vendor_python_path = os.path.join(OPENPYPE_ROOT, "vendor", "python") sys.path.insert(0, vendor_python_path) +# Add common package to sys path +# - common contains common code for bootstraping and OpenPype processes +sys.path.insert(0, os.path.join(OPENPYPE_ROOT, "common")) + import blessed # noqa: E402 import certifi # noqa: E402 diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 300024dc98..f04607dc27 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -12,7 +12,7 @@ import requests import re from tests.lib.db_handler import DBHandler -from common.openpype_common.distribution.file_handler import RemoteFileHandler +from common.ayon_common.distribution.file_handler import RemoteFileHandler from openpype.modules import ModulesManager from openpype.settings import get_project_settings diff --git a/tools/run_tray_ayon.ps1 b/tools/run_tray_ayon.ps1 new file mode 100644 index 0000000000..c0651bdbbe --- /dev/null +++ b/tools/run_tray_ayon.ps1 @@ -0,0 +1,41 @@ +<# +.SYNOPSIS + Helper script OpenPype Tray. + +.DESCRIPTION + + +.EXAMPLE + +PS> .\run_tray.ps1 + +#> +$current_dir = Get-Location +$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent +$openpype_root = (Get-Item $script_dir).parent.FullName + +# Install PSWriteColor to support colorized output to terminal +$env:PSModulePath = $env:PSModulePath + ";$($openpype_root)\tools\modules\powershell" + +$env:_INSIDE_OPENPYPE_TOOL = "1" + +# make sure Poetry is in PATH +if (-not (Test-Path 'env:POETRY_HOME')) { + $env:POETRY_HOME = "$openpype_root\.poetry" +} +$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" + + +Set-Location -Path $openpype_root + +Write-Color -Text ">>> ", "Reading Poetry ... " -Color Green, Gray -NoNewline +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { + Write-Color -Text "NOT FOUND" -Color Yellow + Write-Color -Text "*** ", "We need to install Poetry create virtual env first ..." -Color Yellow, Gray + & "$openpype_root\tools\create_env.ps1" +} else { + Write-Color -Text "OK" -Color Green +} + +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\ayon_start.py" tray --debug +Set-Location -Path $current_dir From e9b1713975a4ca534d2702ecf1c3b9f8983e88c5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 Mar 2023 17:18:11 +0200 Subject: [PATCH 205/446] change 'token_changed' (#4754) --- common/ayon_common/connection/credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/ayon_common/connection/credentials.py b/common/ayon_common/connection/credentials.py index 13d8fe2d7d..4d1a97ee00 100644 --- a/common/ayon_common/connection/credentials.py +++ b/common/ayon_common/connection/credentials.py @@ -34,7 +34,7 @@ class ChangeUserResult: ): shutdown = logged_out restart = new_url is not None and new_url != old_url - token_changed = new_token is not None and new_token == old_token + token_changed = new_token is not None and new_token != old_token self.logged_out = logged_out self.old_url = old_url From db6222fd0db2bf834ca56306706f5db6484a4491 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 Mar 2023 18:10:46 +0200 Subject: [PATCH 206/446] vendorize ayon api (#4753) --- ayon_start.py | 13 + .../vendor/python/ayon/ayon_api/__init__.py | 256 + openpype/vendor/python/ayon/ayon_api/_api.py | 811 ++++ .../vendor/python/ayon/ayon_api/constants.py | 105 + .../vendor/python/ayon/ayon_api/entity_hub.py | 1683 +++++++ .../vendor/python/ayon/ayon_api/events.py | 52 + .../vendor/python/ayon/ayon_api/exceptions.py | 97 + .../vendor/python/ayon/ayon_api/graphql.py | 896 ++++ .../python/ayon/ayon_api/graphql_queries.py | 362 ++ .../vendor/python/ayon/ayon_api/operations.py | 688 +++ .../vendor/python/ayon/ayon_api/server_api.py | 4247 +++++++++++++++++ .../vendor/python/ayon/ayon_api/thumbnails.py | 219 + openpype/vendor/python/ayon/ayon_api/utils.py | 451 ++ .../vendor/python/ayon/ayon_api/version.py | 2 + poetry.lock | 18 - pyproject.toml | 1 - 16 files changed, 9882 insertions(+), 19 deletions(-) create mode 100644 openpype/vendor/python/ayon/ayon_api/__init__.py create mode 100644 openpype/vendor/python/ayon/ayon_api/_api.py create mode 100644 openpype/vendor/python/ayon/ayon_api/constants.py create mode 100644 openpype/vendor/python/ayon/ayon_api/entity_hub.py create mode 100644 openpype/vendor/python/ayon/ayon_api/events.py create mode 100644 openpype/vendor/python/ayon/ayon_api/exceptions.py create mode 100644 openpype/vendor/python/ayon/ayon_api/graphql.py create mode 100644 openpype/vendor/python/ayon/ayon_api/graphql_queries.py create mode 100644 openpype/vendor/python/ayon/ayon_api/operations.py create mode 100644 openpype/vendor/python/ayon/ayon_api/server_api.py create mode 100644 openpype/vendor/python/ayon/ayon_api/thumbnails.py create mode 100644 openpype/vendor/python/ayon/ayon_api/utils.py create mode 100644 openpype/vendor/python/ayon/ayon_api/version.py diff --git a/ayon_start.py b/ayon_start.py index 11677b4415..1e791f4f4f 100644 --- a/ayon_start.py +++ b/ayon_start.py @@ -96,6 +96,19 @@ else: sys.path.append(_dependencies_path) _python_paths.append(_dependencies_path) +# ------------------------------------------------- +# Temporary solution to add ayon_api to python path +# ------------------------------------------------- +# This is to avoid need of new build & release when ayon-python-api is updated. +ayon_dependency_dir = os.path.join( + AYON_ROOT, "openpype", "vendor", "python", "ayon" +) +if ayon_dependency_dir in _python_paths: + _python_paths.remove(ayon_dependency_dir) +_python_paths.insert(0, _dependencies_path) +sys.path.insert(0, ayon_dependency_dir) +# ------------------------------------------------- + # Vendored python modules that must not be in PYTHONPATH environment but # are required for OpenPype processes sys.path.insert(0, os.path.join(AYON_ROOT, "vendor", "python")) diff --git a/openpype/vendor/python/ayon/ayon_api/__init__.py b/openpype/vendor/python/ayon/ayon_api/__init__.py new file mode 100644 index 0000000000..700c1b3687 --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/__init__.py @@ -0,0 +1,256 @@ +from .utils import ( + TransferProgress, + slugify_string, +) +from .server_api import ( + ServerAPI, +) + +from ._api import ( + GlobalServerAPI, + ServiceContext, + + init_service, + get_service_name, + get_service_addon_name, + get_service_addon_version, + get_service_addon_settings, + + is_connection_created, + create_connection, + close_connection, + change_token, + set_environments, + get_server_api_connection, + get_site_id, + set_site_id, + get_client_version, + set_client_version, + get_default_settings_variant, + set_default_settings_variant, + + get_base_url, + get_rest_url, + + raw_get, + raw_post, + raw_put, + raw_patch, + raw_delete, + + get, + post, + put, + patch, + delete, + + get_event, + get_events, + dispatch_event, + update_event, + enroll_event_job, + + download_file, + upload_file, + + query_graphql, + + get_addons_info, + download_addon_private_file, + + get_dependencies_info, + update_dependency_info, + + download_dependency_package, + upload_dependency_package, + delete_dependency_package, + + get_user, + get_users, + + get_attributes_for_type, + get_default_fields_for_type, + + get_project_anatomy_preset, + get_project_anatomy_presets, + get_project_roots_by_site, + get_project_roots_for_site, + + get_addon_site_settings_schema, + get_addon_settings_schema, + + get_addon_studio_settings, + get_addon_project_settings, + get_addon_settings, + get_addons_studio_settings, + get_addons_project_settings, + get_addons_settings, + + get_projects, + get_project, + create_project, + delete_project, + + get_folder_by_id, + get_folder_by_name, + get_folder_by_path, + get_folders, + + get_tasks, + + get_folder_ids_with_subsets, + get_subset_by_id, + get_subset_by_name, + get_subsets, + get_subset_families, + + get_version_by_id, + get_version_by_name, + version_is_latest, + get_versions, + get_hero_version_by_subset_id, + get_hero_version_by_id, + get_hero_versions, + get_last_versions, + get_last_version_by_subset_id, + get_last_version_by_subset_name, + get_representation_by_id, + get_representation_by_name, + get_representations, + get_representations_parents, + get_representation_parents, + get_repre_ids_by_context_filters, + + create_thumbnail, + get_thumbnail, + get_folder_thumbnail, + get_version_thumbnail, + get_workfile_thumbnail, +) + + +__all__ = ( + "TransferProgress", + "slugify_string", + + "ServerAPI", + + "GlobalServerAPI", + "ServiceContext", + + "init_service", + "get_service_name", + "get_service_addon_name", + "get_service_addon_version", + "get_service_addon_settings", + + "is_connection_created", + "create_connection", + "close_connection", + "change_token", + "set_environments", + "get_server_api_connection", + "get_site_id", + "set_site_id", + "get_client_version", + "set_client_version", + "get_default_settings_variant", + "set_default_settings_variant", + + "get_base_url", + "get_rest_url", + + "raw_get", + "raw_post", + "raw_put", + "raw_patch", + "raw_delete", + + "get", + "post", + "put", + "patch", + "delete", + + "get_event", + "get_events", + "dispatch_event", + "update_event", + "enroll_event_job", + + "download_file", + "upload_file", + + "query_graphql", + + "get_addons_info", + "download_addon_private_file", + + "get_dependencies_info", + "update_dependency_info", + + "download_dependency_package", + "upload_dependency_package", + "delete_dependency_package", + + "get_user", + "get_users", + + "get_attributes_for_type", + "get_default_fields_for_type", + + "get_project_anatomy_preset", + "get_project_anatomy_presets", + "get_project_roots_by_site", + "get_project_roots_for_site", + + "get_addon_site_settings_schema", + "get_addon_settings_schema", + "get_addon_studio_settings", + "get_addon_project_settings", + "get_addon_settings", + "get_addons_studio_settings", + "get_addons_project_settings", + "get_addons_settings", + + "get_projects", + "get_project", + "create_project", + "delete_project", + + "get_folder_by_id", + "get_folder_by_name", + "get_folder_by_path", + "get_folders", + + "get_tasks", + + "get_folder_ids_with_subsets", + "get_subset_by_id", + "get_subset_by_name", + "get_subsets", + "get_subset_families", + + "get_version_by_id", + "get_version_by_name", + "version_is_latest", + "get_versions", + "get_hero_version_by_subset_id", + "get_hero_version_by_id", + "get_hero_versions", + "get_last_versions", + "get_last_version_by_subset_id", + "get_last_version_by_subset_name", + "get_representation_by_id", + "get_representation_by_name", + "get_representations", + "get_representations_parents", + "get_representation_parents", + "get_repre_ids_by_context_filters", + + "create_thumbnail", + "get_thumbnail", + "get_folder_thumbnail", + "get_version_thumbnail", + "get_workfile_thumbnail", +) diff --git a/openpype/vendor/python/ayon/ayon_api/_api.py b/openpype/vendor/python/ayon/ayon_api/_api.py new file mode 100644 index 0000000000..6410b459eb --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/_api.py @@ -0,0 +1,811 @@ +"""Singleton based server api for direct access. + +This implementation will be probably the most used part of package. Gives +option to have singleton connection to Server URL based on environment variable +values. All public functions and classes are imported in '__init__.py' so +they're available directly in top module import. +""" + +import os +import socket + +from .constants import ( + SERVER_URL_ENV_KEY, + SERVER_TOKEN_ENV_KEY, +) +from .server_api import ServerAPI +from .exceptions import FailedServiceInit + + +class GlobalServerAPI(ServerAPI): + """Extended server api which also handles storing tokens and url. + + Created object expect to have set environment variables + 'AYON_SERVER_URL'. Also is expecting filled 'AYON_TOKEN' + but that can be filled afterwards with calling 'login' method. + """ + + def __init__(self, site_id=None, client_version=None): + url = self.get_url() + token = self.get_token() + + super(GlobalServerAPI, self).__init__(url, token, site_id, client_version) + + self.validate_server_availability() + self.create_session() + + def login(self, username, password): + """Login to the server or change user. + + If user is the same as current user and token is available the + login is skipped. + """ + + previous_token = self._access_token + super(GlobalServerAPI, self).login(username, password) + if self.has_valid_token and previous_token != self._access_token: + os.environ[SERVER_TOKEN_ENV_KEY] = self._access_token + + @staticmethod + def get_url(): + return os.environ.get(SERVER_URL_ENV_KEY) + + @staticmethod + def get_token(): + return os.environ.get(SERVER_TOKEN_ENV_KEY) + + @staticmethod + def set_environments(url, token): + """Change url and token environemnts in currently running process. + + Args: + url (str): New server url. + token (str): User's token. + """ + + os.environ[SERVER_URL_ENV_KEY] = url or "" + os.environ[SERVER_TOKEN_ENV_KEY] = token or "" + + +class GlobalContext: + """Singleton connection holder. + + Goal is to avoid create connection on import which can be dangerous in + some cases. + """ + + _connection = None + + @classmethod + def is_connection_created(cls): + return cls._connection is not None + + @classmethod + def change_token(cls, url, token): + GlobalServerAPI.set_environments(url, token) + if cls._connection is None: + return + + if cls._connection.get_base_url() == url: + cls._connection.set_token(token) + else: + cls.close_connection() + + @classmethod + def close_connection(cls): + if cls._connection is not None: + cls._connection.close_session() + cls._connection = None + + @classmethod + def create_connection(cls, *args, **kwargs): + if cls._connection is not None: + cls.close_connection() + cls._connection = GlobalServerAPI(*args, **kwargs) + return cls._connection + + @classmethod + def get_server_api_connection(cls): + if cls._connection is None: + cls.create_connection() + return cls._connection + + +class ServiceContext: + """Helper for services running under server. + + When service is running from server the process receives information about + connection from environment variables. This class helps to initialize the + values without knowing environment variables (that may change over time). + + All what must be done is to call 'init_service' function/method. The + arguments are for cases when the service is running in specific environment + and their values are e.g. loaded from private file or for testing purposes. + """ + + token = None + server_url = None + addon_name = None + addon_version = None + service_name = None + + @staticmethod + def get_value_from_envs(env_keys, value=None): + if value: + return value + + for env_key in env_keys: + value = os.environ.get(env_key) + if value: + break + return value + + @classmethod + def init_service( + cls, + token=None, + server_url=None, + addon_name=None, + addon_version=None, + service_name=None, + connect=True + ): + token = cls.get_value_from_envs( + ("AY_API_KEY", "AYON_TOKEN"), + token + ) + server_url = cls.get_value_from_envs( + ("AY_SERVER_URL", "AYON_SERVER_URL"), + server_url + ) + if not server_url: + raise FailedServiceInit("URL to server is not set") + + if not token: + raise FailedServiceInit( + "Token to server {} is not set".format(server_url) + ) + + addon_name = cls.get_value_from_envs( + ("AY_ADDON_NAME", "AYON_ADDON_NAME"), + addon_name + ) + addon_version = cls.get_value_from_envs( + ("AY_ADDON_VERSION", "AYON_ADDON_VERSION"), + addon_version + ) + service_name = cls.get_value_from_envs( + ("AY_SERVICE_NAME", "AYON_SERVICE_NAME"), + service_name + ) + + cls.token = token + cls.server_url = server_url + cls.addon_name = addon_name + cls.addon_version = addon_version + cls.service_name = service_name or socket.gethostname() + + # Make sure required environments for GlobalServerAPI are set + GlobalServerAPI.set_environments(cls.server_url, cls.token) + + if connect: + print("Connecting to server \"{}\"".format(server_url)) + con = GlobalContext.get_server_api_connection() + user = con.get_user() + print("Logged in as user \"{}\"".format(user["name"])) + + +def init_service(*args, **kwargs): + """Initialize current connection from service. + + The service expect specific environment variables. The variables must all + be set to make the connection work as a service. + """ + + ServiceContext.init_service(*args, **kwargs) + + +def get_service_addon_name(): + """Name of addon which initialized service connection. + + Service context must be initialized to be able to use this function. Call + 'init_service' on you service start to do so. + + Returns: + Union[str, None]: Name of addon or None. + """ + + return ServiceContext.addon_name + + +def get_service_addon_version(): + """Version of addon which initialized service connection. + + Service context must be initialized to be able to use this function. Call + 'init_service' on you service start to do so. + + Returns: + Union[str, None]: Version of addon or None. + """ + + return ServiceContext.addon_version + + +def get_service_name(): + """Name of service. + + Service context must be initialized to be able to use this function. Call + 'init_service' on you service start to do so. + + Returns: + Union[str, None]: Name of service if service was registered. + """ + + return ServiceContext.service_name + + +def get_service_addon_settings(): + """Addon settings of service which initialized service. + + Service context must be initialized to be able to use this function. Call + 'init_service' on you service start to do so. + + Returns: + Dict[str, Any]: Addon settings. + + Raises: + ValueError: When service was not initialized. + """ + + addon_name = get_service_addon_name() + addon_version = get_service_addon_version() + if addon_name is None or addon_version is None: + raise ValueError("Service is not initialized") + return get_addon_settings(addon_name, addon_version) + + +def is_connection_created(): + """Is global connection created. + + Returns: + bool: True if connection was connected. + """ + + return GlobalContext.is_connection_created() + + +def create_connection(site_id=None, client_version=None): + """Create global connection. + + Args: + site_id (str): Machine site id/name. + client_version (str): Desktop app version. + + Returns: + GlobalServerAPI: Created connection. + """ + + return GlobalContext.create_connection(site_id, client_version) + + +def close_connection(): + """Close global connection if is connected.""" + + GlobalContext.close_connection() + + +def change_token(url, token): + """Change connection token for url. + + This function can be also used to change url. + + Args: + url (str): Server url. + token (str): API key token. + """ + + GlobalContext.change_token(url, token) + + +def set_environments(url, token): + """Set global environments for global connection. + + Args: + url (Union[str, None]): Url to server or None to unset environments. + token (Union[str, None]): API key token to be used for connection. + """ + + GlobalServerAPI.set_environments(url, token) + + +def get_server_api_connection(): + """Access to global scope object of GlobalServerAPI. + + This access expect to have set environment variables 'AYON_SERVER_URL' + and 'AYON_TOKEN'. + + Returns: + GlobalServerAPI: Object of connection to server. + """ + + return GlobalContext.get_server_api_connection() + + +def get_site_id(): + con = get_server_api_connection() + return con.get_site_id() + + +def set_site_id(site_id): + """Set site id of already connected client connection. + + Site id is human-readable machine id used in AYON desktop application. + + Args: + site_id (Union[str, None]): Site id used in connection. + """ + + con = get_server_api_connection() + con.set_site_id(site_id) + + +def get_client_version(): + """Version of client used to connect to server. + + Client version is AYON client build desktop application. + + Returns: + str: Client version string used in connection. + """ + + con = get_server_api_connection() + return con.get_client_version() + + +def set_client_version(client_version): + """Set version of already connected client connection. + + Client version is version of AYON desktop application. + + Args: + client_version (Union[str, None]): Client version string. + """ + + con = get_server_api_connection() + con.set_client_version(client_version) + + +def get_default_settings_variant(): + """Default variant used for settings. + + Returns: + Union[str, None]: name of variant or None. + """ + + con = get_server_api_connection() + return con.get_client_version() + + +def set_default_settings_variant(variant): + """Change default variant for addon settings. + + Note: + It is recommended to set only 'production' or 'staging' variants + as default variant. + + Args: + variant (Union[str, None]): Settings variant name. + """ + + con = get_server_api_connection() + return con.set_default_settings_variant(variant) + + +def get_base_url(): + con = get_server_api_connection() + return con.get_base_url() + + +def get_rest_url(): + con = get_server_api_connection() + return con.get_rest_url() + + +def raw_get(*args, **kwargs): + con = get_server_api_connection() + return con.raw_get(*args, **kwargs) + + +def raw_post(*args, **kwargs): + con = get_server_api_connection() + return con.raw_post(*args, **kwargs) + + +def raw_put(*args, **kwargs): + con = get_server_api_connection() + return con.raw_put(*args, **kwargs) + + +def raw_patch(*args, **kwargs): + con = get_server_api_connection() + return con.raw_patch(*args, **kwargs) + + +def raw_delete(*args, **kwargs): + con = get_server_api_connection() + return con.raw_delete(*args, **kwargs) + + +def get(*args, **kwargs): + con = get_server_api_connection() + return con.get(*args, **kwargs) + + +def post(*args, **kwargs): + con = get_server_api_connection() + return con.post(*args, **kwargs) + + +def put(*args, **kwargs): + con = get_server_api_connection() + return con.put(*args, **kwargs) + + +def patch(*args, **kwargs): + con = get_server_api_connection() + return con.patch(*args, **kwargs) + + +def delete(*args, **kwargs): + con = get_server_api_connection() + return con.delete(*args, **kwargs) + + +def get_event(*args, **kwargs): + con = get_server_api_connection() + return con.get_event(*args, **kwargs) + + +def get_events(*args, **kwargs): + con = get_server_api_connection() + return con.get_events(*args, **kwargs) + + +def dispatch_event(*args, **kwargs): + con = get_server_api_connection() + return con.dispatch_event(*args, **kwargs) + + +def update_event(*args, **kwargs): + con = get_server_api_connection() + return con.update_event(*args, **kwargs) + + +def enroll_event_job(*args, **kwargs): + con = get_server_api_connection() + return con.enroll_event_job(*args, **kwargs) + + +def download_file(*args, **kwargs): + con = get_server_api_connection() + return con.download_file(*args, **kwargs) + + +def upload_file(*args, **kwargs): + con = get_server_api_connection() + return con.upload_file(*args, **kwargs) + + +def query_graphql(*args, **kwargs): + con = get_server_api_connection() + return con.query_graphql(*args, **kwargs) + + +def get_users(*args, **kwargs): + con = get_server_api_connection() + return con.get_users(*args, **kwargs) + + +def get_user(*args, **kwargs): + con = get_server_api_connection() + return con.get_user(*args, **kwargs) + + +def get_attributes_for_type(*args, **kwargs): + con = get_server_api_connection() + return con.get_attributes_for_type(*args, **kwargs) + + +def get_addons_info(*args, **kwargs): + con = get_server_api_connection() + return con.get_addons_info(*args, **kwargs) + + +def download_addon_private_file(*args, **kwargs): + con = get_server_api_connection() + return con.download_addon_private_file(*args, **kwargs) + + +def get_dependencies_info(*args, **kwargs): + con = get_server_api_connection() + return con.get_dependencies_info(*args, **kwargs) + + +def update_dependency_info(*args, **kwargs): + con = get_server_api_connection() + return con.update_dependency_info(*args, **kwargs) + + +def download_dependency_package(*args, **kwargs): + con = get_server_api_connection() + return con.download_dependency_package(*args, **kwargs) + + +def upload_dependency_package(*args, **kwargs): + con = get_server_api_connection() + return con.upload_dependency_package(*args, **kwargs) + + +def delete_dependency_package(*args, **kwargs): + con = get_server_api_connection() + return con.delete_dependency_package(*args, **kwargs) + + +def get_project_anatomy_presets(*args, **kwargs): + con = get_server_api_connection() + return con.get_project_anatomy_presets(*args, **kwargs) + + +def get_project_anatomy_preset(*args, **kwargs): + con = get_server_api_connection() + return con.get_project_anatomy_preset(*args, **kwargs) + + +def get_project_roots_by_site(*args, **kwargs): + con = get_server_api_connection() + return con.get_project_roots_by_site(*args, **kwargs) + + +def get_project_roots_for_site(*args, **kwargs): + con = get_server_api_connection() + return con.get_project_roots_for_site(*args, **kwargs) + + +def get_addon_settings_schema(*args, **kwargs): + con = get_server_api_connection() + return con.get_addon_settings_schema(*args, **kwargs) + + +def get_addon_site_settings_schema(*args, **kwargs): + con = get_server_api_connection() + return con.get_addon_site_settings_schema(*args, **kwargs) + + +def get_addon_studio_settings(*args, **kwargs): + con = get_server_api_connection() + return con.get_addon_studio_settings(*args, **kwargs) + + +def get_addon_project_settings(*args, **kwargs): + con = get_server_api_connection() + return con.get_addon_project_settings(*args, **kwargs) + + +def get_addon_settings(*args, **kwargs): + con = get_server_api_connection() + return con.get_addon_settings(*args, **kwargs) + + +def get_addon_site_settings(*args, **kwargs): + con = get_server_api_connection() + return con.get_addon_site_settings(*args, **kwargs) + + +def get_addons_studio_settings(*args, **kwargs): + con = get_server_api_connection() + return con.get_addons_studio_settings(*args, **kwargs) + + +def get_addons_project_settings(*args, **kwargs): + con = get_server_api_connection() + return con.get_addons_project_settings(*args, **kwargs) + + +def get_addons_settings(*args, **kwargs): + con = get_server_api_connection() + return con.get_addons_settings(*args, **kwargs) + + +def get_project(*args, **kwargs): + con = get_server_api_connection() + return con.get_project(*args, **kwargs) + + +def get_projects(*args, **kwargs): + con = get_server_api_connection() + return con.get_projects(*args, **kwargs) + + +def get_folders(*args, **kwargs): + con = get_server_api_connection() + return con.get_folders(*args, **kwargs) + + +def get_tasks(*args, **kwargs): + con = get_server_api_connection() + return con.get_tasks(*args, **kwargs) + + +def get_folder_by_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_folder_by_id(*args, **kwargs) + + +def get_folder_by_path(*args, **kwargs): + con = get_server_api_connection() + return con.get_folder_by_path(*args, **kwargs) + + +def get_folder_by_name(*args, **kwargs): + con = get_server_api_connection() + return con.get_folder_by_name(*args, **kwargs) + + +def get_folder_ids_with_subsets(*args, **kwargs): + con = get_server_api_connection() + return con.get_folder_ids_with_subsets(*args, **kwargs) + + +def get_subsets(*args, **kwargs): + con = get_server_api_connection() + return con.get_subsets(*args, **kwargs) + + +def get_subset_by_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_subset_by_id(*args, **kwargs) + + +def get_subset_by_name(*args, **kwargs): + con = get_server_api_connection() + return con.get_subset_by_name(*args, **kwargs) + + +def get_subset_families(*args, **kwargs): + con = get_server_api_connection() + return con.get_subset_families(*args, **kwargs) + + +def get_versions(*args, **kwargs): + con = get_server_api_connection() + return con.get_versions(*args, **kwargs) + + +def get_version_by_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_version_by_id(*args, **kwargs) + + +def get_version_by_name(*args, **kwargs): + con = get_server_api_connection() + return con.get_version_by_name(*args, **kwargs) + + +def get_hero_version_by_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_hero_version_by_id(*args, **kwargs) + + +def get_hero_version_by_subset_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_hero_version_by_subset_id(*args, **kwargs) + + +def get_hero_versions(*args, **kwargs): + con = get_server_api_connection() + return con.get_hero_versions(*args, **kwargs) + + +def get_last_versions(*args, **kwargs): + con = get_server_api_connection() + return con.get_last_versions(*args, **kwargs) + + +def get_last_version_by_subset_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_last_version_by_subset_id(*args, **kwargs) + + +def get_last_version_by_subset_name(*args, **kwargs): + con = get_server_api_connection() + return con.get_last_version_by_subset_name(*args, **kwargs) + + +def version_is_latest(*args, **kwargs): + con = get_server_api_connection() + return con.version_is_latest(*args, **kwargs) + + +def get_representations(*args, **kwargs): + con = get_server_api_connection() + return con.get_representations(*args, **kwargs) + + +def get_representation_by_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_representation_by_id(*args, **kwargs) + + +def get_representation_by_name(*args, **kwargs): + con = get_server_api_connection() + return con.get_representation_by_name(*args, **kwargs) + + +def get_representation_parents(*args, **kwargs): + con = get_server_api_connection() + return con.get_representation_parents(*args, **kwargs) + + +def get_representations_parents(*args, **kwargs): + con = get_server_api_connection() + return con.get_representations_parents(*args, **kwargs) + + +def get_repre_ids_by_context_filters(*args, **kwargs): + con = get_server_api_connection() + return con.get_repre_ids_by_context_filters(*args, **kwargs) + + +def create_project( + project_name, + project_code, + library_project=False, + preset_name=None +): + con = get_server_api_connection() + return con.create_project( + project_name, + project_code, + library_project, + preset_name + ) + + +def delete_project(project_name): + con = get_server_api_connection() + return con.delete_project(project_name) + + +def create_thumbnail(project_name, src_filepath): + con = get_server_api_connection() + return con.create_thumbnail(project_name, src_filepath) + + +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) + + +def get_folder_thumbnail(project_name, folder_id, thumbnail_id=None): + con = get_server_api_connection() + return con.get_folder_thumbnail(project_name, folder_id, thumbnail_id) + + +def get_version_thumbnail(project_name, version_id, thumbnail_id=None): + con = get_server_api_connection() + return con.get_version_thumbnail(project_name, version_id, thumbnail_id) + + +def get_workfile_thumbnail(project_name, workfile_id, thumbnail_id=None): + con = get_server_api_connection() + return con.get_workfile_thumbnail(project_name, workfile_id, thumbnail_id) + + +def create_thumbnail(project_name, src_filepath): + con = get_server_api_connection() + return con.create_thumbnail(project_name, src_filepath) + + +def get_default_fields_for_type(entity_type): + con = get_server_api_connection() + return con.get_default_fields_for_type(entity_type) diff --git a/openpype/vendor/python/ayon/ayon_api/constants.py b/openpype/vendor/python/ayon/ayon_api/constants.py new file mode 100644 index 0000000000..e431af6f9d --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/constants.py @@ -0,0 +1,105 @@ +SERVER_URL_ENV_KEY = "AYON_SERVER_URL" +SERVER_TOKEN_ENV_KEY = "AYON_TOKEN" + +# --- Project --- +DEFAULT_PROJECT_FIELDS = { + "active", + "name", + "code", + "config", + "createdAt", +} + +# --- Folders --- +DEFAULT_FOLDER_FIELDS = { + "id", + "name", + "label", + "folderType", + "path", + "parentId", + "active", + "thumbnailId", +} + +# --- Tasks --- +DEFAULT_TASK_FIELDS = { + "id", + "name", + "label", + "taskType", + "folderId", + "active", + "assignees", +} + +# --- Subsets --- +DEFAULT_SUBSET_FIELDS = { + "id", + "name", + "folderId", + "active", + "family", +} + +# --- Versions --- +DEFAULT_VERSION_FIELDS = { + "id", + "name", + "version", + "subsetId", + "taskId", + "active", + "author", + "thumbnailId", + "createdAt", + "updatedAt", +} + +# --- Representations --- +DEFAULT_REPRESENTATION_FIELDS = { + "id", + "name", + "context", + "createdAt", + "active", + "versionId", +} + +REPRESENTATION_FILES_FIELDS = { + "files.name", + "files.hash", + "files.id", + "files.path", + "files.size", +} + +# --- Workfile info --- +DEFAULT_WORKFILE_INFO_FIELDS = { + "active", + "createdAt", + "createdBy", + "id", + "name", + "path", + "projectName", + "taskId", + "thumbnailId", + "updatedAt", + "updatedBy", +} + +DEFAULT_EVENT_FIELDS = { + "id", + "hash", + "createdAt", + "dependsOn", + "description", + "project", + "retries", + "sender", + "status", + "topic", + "updatedAt", + "user", +} \ No newline at end of file diff --git a/openpype/vendor/python/ayon/ayon_api/entity_hub.py b/openpype/vendor/python/ayon/ayon_api/entity_hub.py new file mode 100644 index 0000000000..76703d2e15 --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/entity_hub.py @@ -0,0 +1,1683 @@ +import copy +import collections +from abc import ABCMeta, abstractmethod, abstractproperty + +import six +from ._api import get_server_api_connection +from .utils import create_entity_id, convert_entity_id + +UNKNOWN_VALUE = object() +PROJECT_PARENT_ID = object() +_NOT_SET = object() + + +class EntityHub(object): + """Helper to create, update or remove entities in project. + + The hub is a guide to operation with folder entities and update of project. + Project entity must already exist on server (can be only updated). + + Object is caching entities queried from server. They won't be required once + they were queried, so it is recommended to create new hub or clear cache + frequently. + + Todos: + Listen to server events about entity changes to be able update already + queried entities. + + Args: + project_name (str): Name of project where changes will happen. + connection (ServerAPI): Connection to server with logged user. + allow_data_changes (bool): This option gives ability to change 'data' + key on entities. This is not recommended as 'data' may be use for + secure information and would also slow down server queries. Content + of 'data' key can't be received only GraphQl. + """ + + def __init__( + self, project_name, connection=None, allow_data_changes=False + ): + if not connection: + connection = get_server_api_connection() + self._connection = connection + + self._project_name = project_name + self._entities_by_id = {} + self._entities_by_parent_id = collections.defaultdict(list) + self._project_entity = UNKNOWN_VALUE + + self._allow_data_changes = allow_data_changes + + self._path_reset_queue = None + + @property + def allow_data_changes(self): + """Entity hub allows changes of 'data' key on entities.""" + + return self._allow_data_changes + + @property + def project_name(self): + return self._project_name + + @property + def project_entity(self): + """Project entity.""" + + if self._project_entity is UNKNOWN_VALUE: + self.fill_project_from_server() + return self._project_entity + + def get_attributes_for_type(self, entity_type): + """Get attributes available for a type. + + Attributes are based on entity types. + + Todos: + Use attribute schema to validate values on entities. + + Args: + entity_type (Literal["project", "folder", "task"]): Entity type + for which should be attributes received. + + Returns: + Dict[str, Dict[str, Any]]: Attribute schemas that are available + for entered entity type. + """ + + return self._connection.get_attributes_for_type(entity_type) + + def get_entity_by_id(self, entity_id): + """Receive entity by its id without entity type. + + The entity must be already existing in cached objects. + + Args: + entity_id (str): Id of entity. + + Returns: + Union[BaseEntity, None]: Entity object or None. + """ + + return self._entities_by_id.get(entity_id) + + def get_folder_by_id(self, entity_id, allow_query=True): + """Get folder entity by id. + + Args: + entity_id (str): Id of folder entity. + allow_query (bool): Try to query entity from server if is not + available in cache. + + Returns: + Union[FolderEntity, None]: Object of folder or 'None'. + """ + + if allow_query: + return self.get_or_query_entity_by_id(entity_id, ["folder"]) + return self._entities_by_id.get(entity_id) + + def get_task_by_id(self, entity_id, allow_query=True): + """Get task entity by id. + + Args: + entity_id (str): Id of task entity. + allow_query (bool): Try to query entity from server if is not + available in cache. + + Returns: + Union[TaskEntity, None]: Object of folder or 'None'. + """ + + if allow_query: + return self.get_or_query_entity_by_id(entity_id, ["task"]) + return self._entities_by_id.get(entity_id) + + def get_or_query_entity_by_id(self, entity_id, entity_types): + """Get or query entity based on it's id and possible entity types. + + This is a helper function when entity id is known but entity type may + have multiple possible options. + + Args: + entity_id (str): Entity id. + entity_types (Iterable[str]): Possible entity types that can the id + represent. e.g. '["folder", "project"]' + """ + + existing_entity = self._entities_by_id.get(entity_id) + if existing_entity is not None: + return existing_entity + + if not entity_types: + return None + + entity_data = None + for entity_type in entity_types: + if entity_type == "folder": + entity_data = self._connection.get_folder_by_id( + self.project_name, + entity_id, + fields=self._get_folder_fields(), + own_attributes=True + ) + elif entity_type == "task": + entity_data = self._connection.get_task_by_id( + self.project_name, + entity_id, + own_attributes=True + ) + else: + raise ValueError( + "Unknonwn entity type \"{}\"".format(entity_type) + ) + + if entity_data: + break + + if not entity_data: + return None + + if entity_type == "folder": + return self.add_folder(entity_data) + elif entity_type == "task": + return self.add_task(entity_data) + + return None + + @property + def entities(self): + for entity in self._entities_by_id.values(): + yield entity + + def add_new_folder(self, *args, created=True, **kwargs): + """Create folder object and add it to entity hub. + + Args: + parent (Union[ProjectEntity, FolderEntity]): Parent of added + folder. + + Returns: + FolderEntity: Added folder entity. + """ + + folder_entity = FolderEntity( + *args, **kwargs, created=created, entity_hub=self + ) + self.add_entity(folder_entity) + return folder_entity + + def add_new_task(self, *args, created=True, **kwargs): + task_entity = TaskEntity( + *args, **kwargs, created=created, entity_hub=self + ) + self.add_entity(task_entity) + return task_entity + + def add_folder(self, folder): + """Create folder object and add it to entity hub. + + Args: + folder (Dict[str, Any]): Folder entity data. + + Returns: + FolderEntity: Added folder entity. + """ + + folder_entity = FolderEntity.from_entity_data(folder, entity_hub=self) + self.add_entity(folder_entity) + return folder_entity + + def add_task(self, task): + """Create task object and add it to entity hub. + + Args: + task (Dict[str, Any]): Task entity data. + + Returns: + TaskEntity: Added task entity. + """ + + task_entity = TaskEntity.from_entity_data(task, entity_hub=self) + self.add_entity(task_entity) + return task_entity + + def add_entity(self, entity): + """Add entity to hub cache. + + Args: + entity (BaseEntity): Entity that should be added to hub's cache. + """ + + self._entities_by_id[entity.id] = entity + parent_children = self._entities_by_parent_id[entity.parent_id] + if entity not in parent_children: + parent_children.append(entity) + + if entity.parent_id is PROJECT_PARENT_ID: + return + + parent = self._entities_by_id.get(entity.parent_id) + if parent is not None: + parent.add_child(entity.id) + + def folder_path_reseted(self, folder_id): + """Method called from 'FolderEntity' on path reset. + + This should reset cache of folder paths on all children entities. + + The path cache is always propagated from top to bottom so if an entity + has not cached path it means that any children can't have it cached. + """ + + if self._path_reset_queue is not None: + self._path_reset_queue.append(folder_id) + return + + self._path_reset_queue = collections.deque() + self._path_reset_queue.append(folder_id) + while self._path_reset_queue: + children = self._entities_by_parent_id[folder_id] + for child in children: + # Get child path but don't trigger cache + path = child.get_path(False) + if path is not None: + # Reset it's path cache if is set + child.reset_path() + else: + self._path_reset_queue.append(child.id) + + self._path_reset_queue = None + + def unset_entity_parent(self, entity_id, parent_id): + entity = self._entities_by_id.get(entity_id) + parent = self._entities_by_id.get(parent_id) + children_ids = UNKNOWN_VALUE + if parent is not None: + children_ids = parent.get_children_ids(False) + + has_set_parent = False + if entity is not None: + has_set_parent = entity.parent_id == parent_id + + new_parent_id = None + if has_set_parent: + entity.parent_id = new_parent_id + + if children_ids is not UNKNOWN_VALUE and entity_id in children_ids: + parent.remove_child(entity_id) + + if entity is None or not has_set_parent: + self.reset_immutable_for_hierarchy_cache(parent_id) + return + + orig_parent_children = self._entities_by_parent_id[parent_id] + if entity in orig_parent_children: + orig_parent_children.remove(entity) + + new_parent_children = self._entities_by_parent_id[new_parent_id] + if entity not in new_parent_children: + new_parent_children.append(entity) + self.reset_immutable_for_hierarchy_cache(parent_id) + + def set_entity_parent(self, entity_id, parent_id, orig_parent_id=_NOT_SET): + parent = self._entities_by_id.get(parent_id) + entity = self._entities_by_id.get(entity_id) + if entity is None: + if parent is not None: + children_ids = parent.get_children_ids(False) + if ( + children_ids is not UNKNOWN_VALUE + and entity_id in children_ids + ): + parent.remove_child(entity_id) + self.reset_immutable_for_hierarchy_cache(parent.id) + return + + if orig_parent_id is _NOT_SET: + orig_parent_id = entity.parent_id + if orig_parent_id == parent_id: + return + + orig_parent_children = self._entities_by_parent_id[orig_parent_id] + if entity in orig_parent_children: + orig_parent_children.remove(entity) + self.reset_immutable_for_hierarchy_cache(orig_parent_id) + + orig_parent = self._entities_by_id.get(orig_parent_id) + if orig_parent is not None: + orig_parent.remove_child(entity_id) + + parent_children = self._entities_by_parent_id[parent_id] + if entity not in parent_children: + parent_children.append(entity) + + entity.parent_id = parent_id + if parent is None or parent.get_children_ids(False) is UNKNOWN_VALUE: + return + + parent.add_child(entity_id) + self.reset_immutable_for_hierarchy_cache(parent_id) + + def _query_entity_children(self, entity): + folder_fields = self._get_folder_fields() + tasks = [] + folders = [] + if entity.entity_type == "project": + folders = list(self._connection.get_folders( + entity["name"], + parent_ids=[entity.id], + fields=folder_fields, + own_attributes=True + )) + + elif entity.entity_type == "folder": + folders = list(self._connection.get_folders( + self.project_entity["name"], + parent_ids=[entity.id], + fields=folder_fields, + own_attributes=True + )) + + tasks = list(self._connection.get_tasks( + self.project_entity["name"], + folder_ids=[entity.id], + own_attributes=True + )) + + children_ids = { + child.id + for child in self._entities_by_parent_id[entity.id] + } + for folder in folders: + folder_entity = self._entities_by_id.get(folder["id"]) + if folder_entity is not None: + if folder_entity.parent_id == entity.id: + children_ids.add(folder_entity.id) + continue + + folder_entity = self.add_folder(folder) + children_ids.add(folder_entity.id) + + for task in tasks: + task_entity = self._entities_by_id.get(task["id"]) + if task_entity is not None: + if task_entity.parent_id == entity.id: + children_ids.add(task_entity.id) + continue + + task_entity = self.add_task(task) + children_ids.add(task_entity.id) + + entity.fill_children_ids(children_ids) + + def get_entity_children(self, entity, allow_query=True): + children_ids = entity.get_children_ids(allow_query=False) + if children_ids is not UNKNOWN_VALUE: + return entity.get_children() + + if children_ids is UNKNOWN_VALUE and not allow_query: + return UNKNOWN_VALUE + + self._query_entity_children(entity) + + return entity.get_children() + + def delete_entity(self, entity): + parent_id = entity.parent_id + if parent_id is None: + return + + parent = self._entities_by_parent_id.get(parent_id) + if parent is not None: + parent.remove_child(entity.id) + + def reset_immutable_for_hierarchy_cache( + self, entity_id, bottom_to_top=True + ): + if bottom_to_top is None or entity_id is None: + return + + reset_queue = collections.deque() + reset_queue.append(entity_id) + if bottom_to_top: + while reset_queue: + entity_id = reset_queue.popleft() + entity = self.get_entity_by_id(entity_id) + if entity is None: + continue + entity.reset_immutable_for_hierarchy_cache(None) + reset_queue.append(entity.parent_id) + else: + while reset_queue: + entity_id = reset_queue.popleft() + entity = self.get_entity_by_id(entity_id) + if entity is None: + continue + entity.reset_immutable_for_hierarchy_cache(None) + for child in self._entities_by_parent_id[entity.id]: + reset_queue.append(child.id) + + def fill_project_from_server(self): + """Query project from server and create it's entity. + + Returns: + ProjectEntity: Entity that was created based on queried data. + + Raises: + ValueError: When project was not found on server. + """ + + project_name = self.project_name + project = self._connection.get_project( + project_name, + own_attributes=True + ) + if not project: + raise ValueError( + "Project \"{}\" was not found.".format(project_name) + ) + + self._project_entity = ProjectEntity( + project["code"], + parent_id=PROJECT_PARENT_ID, + entity_id=project["name"], + library=project["library"], + folder_types=project["folderTypes"], + task_types=project["taskTypes"], + name=project["name"], + attribs=project["ownAttrib"], + data=project["data"], + active=project["active"], + entity_hub=self + ) + self.add_entity(self._project_entity) + return self._project_entity + + def _get_folder_fields(self): + folder_fields = set( + self._connection.get_default_fields_for_type("folder") + ) + folder_fields.add("hasSubsets") + if self._allow_data_changes: + folder_fields.add("data") + return folder_fields + + def query_entities_from_server(self): + """Query whole project at once.""" + + project_entity = self.fill_project_from_server() + + folder_fields = self._get_folder_fields() + + folders = self._connection.get_folders( + project_entity.name, + fields=folder_fields, + own_attributes=True + ) + tasks = self._connection.get_tasks( + project_entity.name, + own_attributes=True + ) + folders_by_parent_id = collections.defaultdict(list) + for folder in folders: + parent_id = folder["parentId"] + folders_by_parent_id[parent_id].append(folder) + + tasks_by_parent_id = collections.defaultdict(list) + for task in tasks: + parent_id = task["folderId"] + tasks_by_parent_id[parent_id].append(task) + + hierarchy_queue = collections.deque() + hierarchy_queue.append((None, project_entity)) + while hierarchy_queue: + item = hierarchy_queue.popleft() + parent_id, parent_entity = item + + children_ids = set() + for folder in folders_by_parent_id[parent_id]: + folder_entity = self.add_folder(folder) + children_ids.add(folder_entity.id) + folder_entity.has_published_content = folder["hasSubsets"] + hierarchy_queue.append((folder_entity.id, folder_entity)) + + for task in tasks_by_parent_id[parent_id]: + task_entity = self.add_task(task) + children_ids.add(task_entity.id) + + parent_entity.fill_children_ids(children_ids) + self.lock() + + def lock(self): + if self._project_entity is None: + return + + for entity in self._entities_by_id.values(): + entity.lock() + + def _get_top_entities(self): + all_ids = set(self._entities_by_id.keys()) + return [ + entity + for entity in self._entities_by_id.values() + if entity.parent_id not in all_ids + ] + + def _split_entities(self): + top_entities = self._get_top_entities() + entities_queue = collections.deque(top_entities) + removed_entity_ids = [] + created_entity_ids = [] + other_entity_ids = [] + while entities_queue: + entity = entities_queue.popleft() + removed = entity.removed + if removed: + removed_entity_ids.append(entity.id) + elif entity.created: + created_entity_ids.append(entity.id) + else: + other_entity_ids.append(entity.id) + + for child in tuple(self._entities_by_parent_id[entity.id]): + if removed: + self.unset_entity_parent(child.id, entity.id) + entities_queue.append(child) + return created_entity_ids, other_entity_ids, removed_entity_ids + + def _get_update_body(self, entity, changes=None): + if changes is None: + changes = entity.changes + + if not changes: + return None + return { + "type": "update", + "entityType": entity.entity_type, + "entityId": entity.id, + "data": changes + } + + def _get_create_body(self, entity): + return { + "type": "create", + "entityType": entity.entity_type, + "entityId": entity.id, + "data": entity.to_create_body_data() + } + + def _get_delete_body(self, entity): + return { + "type": "delete", + "entityType": entity.entity_type, + "entityId": entity.id + } + + def commit_changes(self): + """Commit any changes that happened on entities. + + Todos: + Use Operations Session instead of known operations body. + """ + + project_changes = self.project_entity.changes + if project_changes: + response = self._connection.patch( + "projects/{}".format(self.project_name), + **project_changes + ) + if response.status_code != 204: + raise ValueError("Failed to update project") + + self.project_entity.lock() + + operations_body = [] + + created_entity_ids, other_entity_ids, removed_entity_ids = ( + self._split_entities() + ) + processed_ids = set() + for entity_id in other_entity_ids: + if entity_id in processed_ids: + continue + + entity = self._entities_by_id[entity_id] + changes = entity.changes + processed_ids.add(entity_id) + if not changes: + continue + + bodies = [self._get_update_body(entity, changes)] + # Parent was created and was not yet added to operations body + parent_queue = collections.deque() + parent_queue.append(entity.parent_id) + while parent_queue: + # Make sure entity's parents are created + parent_id = parent_queue.popleft() + if ( + parent_id is UNKNOWN_VALUE + or parent_id in processed_ids + or parent_id not in created_entity_ids + ): + continue + + parent = self._entities_by_id.get(parent_id) + processed_ids.add(parent.id) + bodies.append(self._get_create_body(parent)) + parent_queue.append(parent.id) + + operations_body.extend(reversed(bodies)) + + for entity_id in created_entity_ids: + if entity_id in processed_ids: + continue + entity = self._entities_by_id[entity_id] + processed_ids.add(entity_id) + operations_body.append(self._get_create_body(entity)) + + for entity_id in reversed(removed_entity_ids): + if entity_id in processed_ids: + continue + + entity = self._entities_by_id.pop(entity_id) + parent_children = self._entities_by_parent_id[entity.parent_id] + if entity in parent_children: + parent_children.remove(entity) + + if not entity.created: + operations_body.append(self._get_delete_body(entity)) + + self._connection.send_batch_operations( + self.project_name, operations_body + ) + + self.lock() + + +class AttributeValue(object): + def __init__(self, value): + self._value = value + self._origin_value = copy.deepcopy(value) + + def get_value(self): + return self._value + + def set_value(self, value): + self._value = value + + value = property(get_value, set_value) + + @property + def changed(self): + return self._value != self._origin_value + + def lock(self): + self._origin_value = copy.deepcopy(self._value) + + +class Attributes(object): + """Object representing attribs of entity. + + Todos: + This could be enhanced to know attribute schema and validate values + based on the schema. + + Args: + attrib_keys (Iterable[str]): Keys that are available in attribs of the + entity. + values (Union[None, Dict[str, Any]]): Values of attributes. + """ + + def __init__(self, attrib_keys, values=UNKNOWN_VALUE): + if values in (UNKNOWN_VALUE, None): + values = {} + self._attributes = { + key: AttributeValue(values.get(key)) + for key in attrib_keys + } + + def __contains__(self, key): + return key in self._attributes + + def __getitem__(self, key): + return self._attributes[key].value + + def __setitem__(self, key, value): + self._attributes[key].set_value(value) + + def __iter__(self): + for key in self._attributes: + yield key + + def keys(self): + return self._attributes.keys() + + def values(self): + for attribute in self._attributes.values(): + yield attribute.value + + def items(self): + for key, attribute in self._attributes.items(): + yield key, attribute.value + + def get(self, key, default=None): + """Get value of attribute. + + Args: + key (str): Attribute name. + default (Any): Default value to return when attribute was not + found. + """ + + attribute = self._attributes.get(key) + if attribute is None: + return default + return attribute.value + + def set(self, key, value): + """Change value of attribute. + + Args: + key (str): Attribute name. + value (Any): New value of the attribute. + """ + + self[key] = value + + def get_attribute(self, key): + """Access to attribute object. + + Args: + key (str): Name of attribute. + + Returns: + AttributeValue: Object of attribute value. + + Raises: + KeyError: When attribute is not available. + """ + + return self._attributes[key] + + def lock(self): + for attribute in self._attributes.values(): + attribute.lock() + + @property + def changes(self): + """Attribute value changes. + + Returns: + Dict[str, Any]: Key mapping with new values. + """ + + return { + attr_key: attribute.value + for attr_key, attribute in self._attributes.items() + if attribute.changed + } + + def to_dict(self, ignore_none=True): + output = {} + for key, value in self.items(): + if ( + value is UNKNOWN_VALUE + or (ignore_none and value is None) + ): + continue + + output[key] = value + return output + + +@six.add_metaclass(ABCMeta) +class BaseEntity(object): + """Object representation of entity from server which is capturing changes. + + All data on created object are expected as "current data" on server entity + unless the entity has set 'created' to 'True'. So if new data should be + stored to server entity then fill entity with server data first and + then change them. + + Calling 'lock' method will mark entity as "saved" and all changes made on + entity are set as "current data" on server. + + Args: + name (str): Name of entity. + attribs (Dict[str, Any]): Attribute values. + data (Dict[str, Any]): Entity data (custom data). + parent_id (Union[str, None]): Id of parent entity. + entity_id (Union[str, None]): Id of the entity. New id is created if + not passed. + thumbnail_id (Union[str, None]): Id of entity's thumbnail. + active (bool): Is entity active. + entity_hub (EntityHub): Object of entity hub which created object of + the entity. + created (Union[bool, None]): Entity is new. When 'None' is passed the + value is defined based on value of 'entity_id'. + """ + + def __init__( + self, + entity_id=None, + parent_id=UNKNOWN_VALUE, + name=UNKNOWN_VALUE, + attribs=UNKNOWN_VALUE, + data=UNKNOWN_VALUE, + thumbnail_id=UNKNOWN_VALUE, + active=UNKNOWN_VALUE, + entity_hub=None, + created=None + ): + if entity_hub is None: + raise ValueError("Missing required kwarg 'entity_hub'") + + self._entity_hub = entity_hub + + if created is None: + created = entity_id is None + + entity_id = self._prepare_entity_id(entity_id) + + if data is None: + data = {} + + children_ids = UNKNOWN_VALUE + if created: + children_ids = set() + + if not created and parent_id is UNKNOWN_VALUE: + raise ValueError("Existing entity is missing parent id.") + + # These are public without any validation at this moment + # may change in future (e.g. name will have regex validation) + self._entity_id = entity_id + + self._parent_id = parent_id + self._name = name + self.active = active + self._created = created + self._thumbnail_id = thumbnail_id + self._attribs = Attributes( + self._get_attributes_for_type(self.entity_type), + attribs + ) + self._data = data + self._children_ids = children_ids + + self._orig_parent_id = parent_id + self._orig_name = name + self._orig_data = copy.deepcopy(data) + self._orig_thumbnail_id = thumbnail_id + self._orig_active = active + + self._immutable_for_hierarchy_cache = None + + def __repr__(self): + return "<{} - {}>".format(self.__class__.__name__, self.id) + + def __getitem__(self, item): + return getattr(self, item) + + def __setitem__(self, item, value): + return setattr(self, item, value) + + def _prepare_entity_id(self, entity_id): + entity_id = convert_entity_id(entity_id) + if entity_id is None: + entity_id = create_entity_id() + return entity_id + + @property + def id(self): + """Access to entity id under which is entity available on server. + + Returns: + str: Entity id. + """ + + return self._entity_id + + @property + def removed(self): + return self._parent_id is None + + @property + def orig_parent_id(self): + return self._orig_parent_id + + @property + def attribs(self): + """Entity attributes based on server configuration. + + Returns: + Attributes: Attributes object handling changes and values of + attributes on entity. + """ + + return self._attribs + + @property + def data(self): + """Entity custom data that are not stored by any deterministic model. + + Be aware that 'data' can't be queried using GraphQl and cannot be + updated partially. + + Returns: + Dict[str, Any]: Custom data on entity. + """ + + return self._data + + @property + def project_name(self): + """Quick access to project from entity hub. + + Returns: + str: Name of project under which entity lives. + """ + + return self._entity_hub.project_name + + @abstractproperty + def entity_type(self): + """Entity type coresponding to server. + + Returns: + Literal[project, folder, task]: Entity type. + """ + + pass + + @abstractproperty + def parent_entity_types(self): + """Entity type coresponding to server. + + Returns: + Iterable[str]: Possible entity types of parent. + """ + + pass + + @abstractproperty + def changes(self): + """Receive entity changes. + + Returns: + Union[Dict[str, Any], None]: All values that have changed on + entity. New entity must return None. + """ + + pass + + @classmethod + @abstractmethod + def from_entity_data(cls, entity_data, entity_hub): + """Create entity based on queried data from server. + + Args: + entity_data (Dict[str, Any]): Entity data from server. + entity_hub (EntityHub): Hub which handle the entity. + + Returns: + BaseEntity: Object of the class. + """ + + pass + + @abstractmethod + def to_create_body_data(self): + """Convert object of entity to data for server on creation. + + Returns: + Dict[str, Any]: Entity data. + """ + + pass + + @property + def immutable_for_hierarchy(self): + """Entity is immutable for hierarchy changes. + + Hierarchy changes can be considered as change of name or parents. + + Returns: + bool: Entity is immutable for hierarchy changes. + """ + + if self._immutable_for_hierarchy_cache is not None: + return self._immutable_for_hierarchy_cache + + immutable_for_hierarchy = self._immutable_for_hierarchy + if immutable_for_hierarchy is not None: + self._immutable_for_hierarchy_cache = immutable_for_hierarchy + return self._immutable_for_hierarchy_cache + + for child in self._entity_hub.get_entity_children(self): + if child.immutable_for_hierarchy: + self._immutable_for_hierarchy_cache = True + return self._immutable_for_hierarchy_cache + + self._immutable_for_hierarchy_cache = False + return self._immutable_for_hierarchy_cache + + @property + def _immutable_for_hierarchy(self): + """Override this method to define if entity object is immutable. + + This property was added to define immutable state of Folder entities + which is used in property 'immutable_for_hierarchy'. + + Returns: + Union[bool, None]: Bool to explicitly telling if is immutable or + not otherwise None. + """ + + return None + + @property + def has_cached_immutable_hierarchy(self): + return self._immutable_for_hierarchy_cache is not None + + def reset_immutable_for_hierarchy_cache(self, bottom_to_top=True): + """Clear cache of immutable hierarchy property. + + This is used when entity changed parent or a child was added. + + Args: + bottom_to_top (bool): Reset cache from top hierarchy to bottom or + from bottom hierarchy to top. + """ + + self._immutable_for_hierarchy_cache = None + self._entity_hub.reset_immutable_for_hierarchy_cache( + self.id, bottom_to_top + ) + + def _get_default_changes(self): + """Collect changes of common data on entity. + + Returns: + Dict[str, Any]: Changes on entity. Key and it's new value. + """ + + changes = {} + if self._orig_name != self._name: + changes["name"] = self._name + + if self._entity_hub.allow_data_changes: + if self._orig_data != self._data: + changes["data"] = self._data + + if self._orig_thumbnail_id != self._thumbnail_id: + changes["thumbnailId"] = self._thumbnail_id + + if self._orig_active != self.active: + changes["active"] = self.active + + attrib_changes = self.attribs.changes + if attrib_changes: + changes["attrib"] = attrib_changes + return changes + + def _get_attributes_for_type(self, entity_type): + return self._entity_hub.get_attributes_for_type(entity_type) + + def lock(self): + """Lock entity as 'saved' so all changes are discarded.""" + + self._orig_parent_id = self._parent_id + self._orig_name = self._name + self._orig_data = copy.deepcopy(self._data) + self._orig_thumbnail_id = self.thumbnail_id + self._attribs.lock() + + self._immutable_for_hierarchy_cache = None + + def _get_entity_by_id(self, entity_id): + return self._entity_hub.get_entity_by_id(entity_id) + + def get_name(self): + return self._name + + def set_name(self, name): + self._name = name + + name = property(get_name, set_name) + + def get_parent_id(self): + """Parent entity id. + + Returns: + Union[str, None]: Id of parent entity or none if is not set. + """ + + return self._parent_id + + def set_parent_id(self, parent_id): + """Change parent by id. + + Args: + parent_id (Union[str, None]): Id of new parent for entity. + + Raises: + ValueError: If parent was not found by id. + TypeError: If validation of parent does not pass. + """ + + if parent_id != self._parent_id: + orig_parent_id = self._parent_id + self._parent_id = parent_id + self._entity_hub.set_entity_parent( + self.id, parent_id, orig_parent_id + ) + + parent_id = property(get_parent_id, set_parent_id) + + def get_parent(self, allow_query=True): + """Parent entity. + + Returns: + Union[BaseEntity, None]: Parent object. + """ + + parent = self._entity_hub.get_entity_by_id(self._parent_id) + if parent is not None: + return parent + + if not allow_query: + return self._parent_id + + if self._parent_id is UNKNOWN_VALUE: + return self._parent_id + + return self._entity_hub.get_or_query_entity_by_id( + self._parent_id, self.parent_entity_types + ) + + def set_parent(self, parent): + """Change parent object. + + Args: + parent (BaseEntity): New parent for entity. + + Raises: + TypeError: If validation of parent does not pass. + """ + + parent_id = None + if parent is not None: + parent_id = parent.id + self._entity_hub.set_entity_parent(self.id, parent_id) + + parent = property(get_parent, set_parent) + + def get_children_ids(self, allow_query=True): + """Access to children objects. + + Todos: + Children should be maybe handled by EntityHub instead of entities + themselves. That would simplify 'set_entity_parent', + 'unset_entity_parent' and other logic related to changing + hierarchy. + + Returns: + Union[List[str], Type[UNKNOWN_VALUE]]: Children iterator. + """ + + if self._children_ids is UNKNOWN_VALUE: + if not allow_query: + return self._children_ids + self._entity_hub.get_entity_children(self, True) + return set(self._children_ids) + + children_ids = property(get_children_ids) + + def get_children(self, allow_query=True): + """Access to children objects. + + Returns: + Union[List[BaseEntity], Type[UNKNOWN_VALUE]]: Children iterator. + """ + + if self._children_ids is UNKNOWN_VALUE: + if not allow_query: + return self._children_ids + return self._entity_hub.get_entity_children(self, True) + + return [ + self._entity_hub.get_entity_by_id(children_id) + for children_id in self._children_ids + ] + + children = property(get_children) + + def add_child(self, child): + """Add child entity. + + Args: + child (BaseEntity): Child object to add. + + Raises: + TypeError: When child object has invalid type to be children. + """ + + child_id = child + if isinstance(child_id, BaseEntity): + child_id = child.id + + if self._children_ids is not UNKNOWN_VALUE: + self._children_ids.add(child_id) + + self._entity_hub.set_entity_parent(child_id, self.id) + + def remove_child(self, child): + """Remove child entity. + + Is ignored if child is not in children. + + Args: + child (Union[str, BaseEntity]): Child object or child id to remove. + """ + + child_id = child + if isinstance(child_id, BaseEntity): + child_id = child.id + + if self._children_ids is not UNKNOWN_VALUE: + self._children_ids.discard(child_id) + self._entity_hub.unset_entity_parent(child_id, self.id) + + def get_thumbnail_id(self): + """Thumbnail id of entity. + + Returns: + Union[str, None]: Id of parent entity or none if is not set. + """ + + return self._thumbnail_id + + def set_thumbnail_id(self, thumbnail_id): + """Change thumbnail id. + + Args: + thumbnail_id (Union[str, None]): Id of thumbnail for entity. + """ + + self._thumbnail_id = thumbnail_id + + thumbnail_id = property(get_thumbnail_id, set_thumbnail_id) + + @property + def created(self): + """Entity is new. + + Returns: + bool: Entity is newly created. + """ + + return self._created + + def fill_children_ids(self, children_ids): + """Fill children ids on entity. + + Warning: + This is not an api call but is called from entity hub. + """ + + self._children_ids = set(children_ids) + + +class ProjectEntity(BaseEntity): + entity_type = "project" + parent_entity_types = [] + # TODO These are hardcoded but maybe should be used from server??? + default_folder_type_icon = "folder" + default_task_type_icon = "task_alt" + + def __init__( + self, project_code, library, folder_types, task_types, *args, **kwargs + ): + super(ProjectEntity, self).__init__(*args, **kwargs) + + self._project_code = project_code + self._library_project = library + self._folder_types = folder_types + self._task_types = task_types + + 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) + + def _prepare_entity_id(self, entity_id): + if entity_id != self.project_name: + raise ValueError( + "Unexpected entity id value \"{}\". Expected \"{}\"".format( + entity_id, self.project_name)) + return entity_id + + def get_parent(self, *args, **kwargs): + return None + + def set_parent(self, parent): + raise ValueError( + "Parent of project cannot be set to {}".format(parent) + ) + + parent = property(get_parent, set_parent) + + def get_folder_types(self): + return copy.deepcopy(self._folder_types) + + def set_folder_types(self, folder_types): + new_folder_types = [] + for folder_type in folder_types: + if "icon" not in folder_type: + folder_type["icon"] = self.default_folder_type_icon + new_folder_types.append(folder_type) + self._folder_types = new_folder_types + + def get_task_types(self): + return copy.deepcopy(self._task_types) + + def set_task_types(self, task_types): + new_task_types = [] + for task_type in task_types: + if "icon" not in task_type: + task_type["icon"] = self.default_task_type_icon + new_task_types.append(task_type) + self._task_types = new_task_types + + folder_types = property(get_folder_types, set_folder_types) + task_types = property(get_task_types, set_task_types) + + 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) + + @property + def changes(self): + changes = self._get_default_changes() + if self._orig_folder_types != self._folder_types: + changes["folderTypes"] = self.get_folder_types() + + if self._orig_task_types != self._task_types: + changes["taskTypes"] = self.get_task_types() + + return changes + + @classmethod + def from_entity_data(cls, project, entity_hub): + return cls( + project["code"], + parent_id=PROJECT_PARENT_ID, + entity_id=project["name"], + library=project["library"], + folder_types=project["folderTypes"], + task_types=project["taskTypes"], + name=project["name"], + attribs=project["ownAttrib"], + data=project["data"], + active=project["active"], + entity_hub=entity_hub + ) + + def to_create_body_data(self): + raise NotImplementedError( + "ProjectEntity does not support conversion to entity data" + ) + + +class FolderEntity(BaseEntity): + entity_type = "folder" + parent_entity_types = ["folder", "project"] + + def __init__(self, folder_type, *args, label=None, path=None, **kwargs): + super(FolderEntity, self).__init__(*args, **kwargs) + # Autofill project as parent of folder if is not yet set + # - this can be guessed only if folder was just created + if self.created and self._parent_id is UNKNOWN_VALUE: + self._parent_id = self.project_name + + self._folder_type = folder_type + self._label = label + + self._orig_folder_type = folder_type + self._orig_label = label + # Know if folder has any subsets + # - is used to know if folder allows hierarchy changes + self._has_published_content = False + self._path = path + + def get_folder_type(self): + return self._folder_type + + def set_folder_type(self, folder_type): + self._folder_type = folder_type + + folder_type = property(get_folder_type, set_folder_type) + + def get_label(self): + return self._label + + def set_label(self, label): + self._label = label + + label = property(get_label, set_label) + + def get_path(self, dynamic_value=True): + if not dynamic_value: + return self._path + + if self._path is None: + parent = self.parent + path = self.name + if parent.entity_type == "folder": + parent_path = parent.path + path = "/".join([parent_path, path]) + self._path = path + return self._path + + def reset_path(self): + self._path = None + self._entity_hub.folder_path_reseted(self.id) + + path = property(get_path) + + def get_has_published_content(self): + return self._has_published_content + + def set_has_published_content(self, has_published_content): + if self._has_published_content is has_published_content: + return + + self._has_published_content = has_published_content + # Reset immutable cache of parents + self._entity_hub.reset_immutable_for_hierarchy_cache(self.id) + + has_published_content = property( + get_has_published_content, set_has_published_content + ) + + @property + def _immutable_for_hierarchy(self): + if self.has_published_content: + return True + return None + + def lock(self): + super(FolderEntity, self).lock() + self._orig_folder_type = self._folder_type + + @property + def changes(self): + changes = self._get_default_changes() + + if self._orig_parent_id != self._parent_id: + parent_id = self._parent_id + if parent_id == self.project_name: + parent_id = None + changes["parentId"] = parent_id + + if self._orig_folder_type != self._folder_type: + changes["folderType"] = self._folder_type + + label = self._label + if self._name == label: + label = None + + if label != self._orig_label: + changes["label"] = label + + return changes + + @classmethod + def from_entity_data(cls, folder, entity_hub): + parent_id = folder["parentId"] + if parent_id is None: + parent_id = entity_hub.project_entity.id + return cls( + folder["folderType"], + label=folder["label"], + path=folder["path"], + entity_id=folder["id"], + parent_id=parent_id, + name=folder["name"], + data=folder.get("data"), + attribs=folder["ownAttrib"], + active=folder["active"], + thumbnail_id=folder["thumbnailId"], + created=False, + entity_hub=entity_hub + ) + + def to_create_body_data(self): + parent_id = self._parent_id + if parent_id is UNKNOWN_VALUE: + raise ValueError("Folder does not have set 'parent_id'") + + if parent_id == self.project_name: + parent_id = None + + if not self.name or self.name is UNKNOWN_VALUE: + raise ValueError("Folder does not have set 'name'") + + output = { + "name": self.name, + "folderType": self.folder_type, + "parentId": parent_id, + } + attrib = self.attribs.to_dict() + if attrib: + output["attrib"] = attrib + + if self.active is not UNKNOWN_VALUE: + output["active"] = self.active + + if self.thumbnail_id is not UNKNOWN_VALUE: + output["thumbnailId"] = self.thumbnail_id + + if self._entity_hub.allow_data_changes: + output["data"] = self._data + return output + + +class TaskEntity(BaseEntity): + entity_type = "task" + parent_entity_types = ["folder"] + + def __init__(self, task_type, *args, label=None, **kwargs): + super(TaskEntity, self).__init__(*args, **kwargs) + + self._task_type = task_type + self._label = label + + self._orig_task_type = task_type + self._orig_label = label + + self._children_ids = set() + + def lock(self): + super(TaskEntity, self).lock() + self._orig_task_type = self._task_type + + def get_task_type(self): + return self._task_type + + def set_task_type(self, task_type): + self._task_type = task_type + + task_type = property(get_task_type, set_task_type) + + def get_label(self): + return self._label + + def set_label(self, label): + self._label = label + + label = property(get_label, set_label) + + def add_child(self, child): + raise ValueError("Task does not support to add children") + + @property + def changes(self): + changes = self._get_default_changes() + + if self._orig_parent_id != self._parent_id: + changes["folderId"] = self._parent_id + + if self._orig_task_type != self._task_type: + changes["taskType"] = self._task_type + + label = self._label + if self._name == label: + label = None + + if label != self._orig_label: + changes["label"] = label + + return changes + + @classmethod + def from_entity_data(cls, task, entity_hub): + return cls( + task["taskType"], + entity_id=task["id"], + label=task["label"], + parent_id=task["folderId"], + name=task["name"], + data=task.get("data"), + attribs=task["ownAttrib"], + active=task["active"], + created=False, + entity_hub=entity_hub + ) + + def to_create_body_data(self): + if self.parent_id is UNKNOWN_VALUE: + raise ValueError("Task does not have set 'parent_id'") + + output = { + "name": self.name, + "taskType": self.task_type, + "folderId": self.parent_id, + "attrib": self.attribs.to_dict(), + } + attrib = self.attribs.to_dict() + if attrib: + output["attrib"] = attrib + + if self.active is not UNKNOWN_VALUE: + output["active"] = self.active + + if ( + self._entity_hub.allow_data_changes + and self._data is not UNKNOWN_VALUE + ): + output["data"] = self._data + return output diff --git a/openpype/vendor/python/ayon/ayon_api/events.py b/openpype/vendor/python/ayon/ayon_api/events.py new file mode 100644 index 0000000000..1ea9331244 --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/events.py @@ -0,0 +1,52 @@ +import copy + + +class ServerEvent(object): + def __init__( + self, + topic, + sender=None, + event_hash=None, + project_name=None, + username=None, + dependencies=None, + description=None, + summary=None, + payload=None, + finished=True, + store=True, + ): + if dependencies is None: + dependencies = [] + if payload is None: + payload = {} + if summary is None: + summary = {} + + self.topic = topic + self.sender = sender + self.event_hash = event_hash + self.project_name = project_name + self.username = username + self.dependencies = dependencies + self.description = description + self.summary = summary + self.payload = payload + self.finished = finished + self.store = store + + def to_data(self): + return { + "topic": self.topic, + "sender": self.sender, + "hash": self.event_hash, + "project": self.project_name, + "user": self.username, + "dependencies": copy.deepcopy(self.dependencies), + "description": self.description, + "description": self.description, + "summary": copy.deepcopy(self.summary), + "payload": self.payload, + "finished": self.finished, + "store": self.store + } \ No newline at end of file diff --git a/openpype/vendor/python/ayon/ayon_api/exceptions.py b/openpype/vendor/python/ayon/ayon_api/exceptions.py new file mode 100644 index 0000000000..0ff09770b5 --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/exceptions.py @@ -0,0 +1,97 @@ +import copy + + +class UrlError(Exception): + """Url cannot be parsed as url. + + Exception may contain hints of possible fixes of url that can be used in + UI if needed. + """ + + def __init__(self, message, title, hints=None): + if hints is None: + hints = [] + + self.title = title + self.hints = hints + super(UrlError, self).__init__(message) + + +class ServerError(Exception): + pass + + +class UnauthorizedError(ServerError): + pass + + +class AuthenticationError(ServerError): + pass + + +class ServerNotReached(ServerError): + pass + + +class GraphQlQueryFailed(Exception): + def __init__(self, errors, query, variables): + if variables is None: + variables = {} + + error_messages = [] + for error in errors: + msg = error["message"] + path = error.get("path") + if path: + msg += " on item '{}'".format("/".join(path)) + locations = error.get("locations") + if locations: + _locations = [ + "Line {} Column {}".format( + location["line"], location["column"] + ) + for location in locations + ] + + msg += " ({})".format(" and ".join(_locations)) + error_messages.append(msg) + + message = "GraphQl query Failed" + if error_messages: + message = "{}: {}".format(message, " | ".join(error_messages)) + + self.errors = errors + self.query = query + self.variables = copy.deepcopy(variables) + super(GraphQlQueryFailed, self).__init__(message) + + +class MissingEntityError(Exception): + pass + + +class ProjectNotFound(MissingEntityError): + def __init__(self, project_name, message=None): + if not message: + message = "Project \"{}\" was not found".format(project_name) + self.project_name = project_name + super(ProjectNotFound, self).__init__(message) + + +class FolderNotFound(MissingEntityError): + def __init__(self, project_name, folder_id, message=None): + self.project_name = project_name + self.folder_id = folder_id + if not message: + message = ( + "Folder with id \"{}\" was not found in project \"{}\"" + ).format(folder_id, project_name) + super(FolderNotFound, self).__init__(message) + + +class FailedOperations(Exception): + pass + + +class FailedServiceInit(Exception): + pass \ No newline at end of file diff --git a/openpype/vendor/python/ayon/ayon_api/graphql.py b/openpype/vendor/python/ayon/ayon_api/graphql.py new file mode 100644 index 0000000000..93349e9608 --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/graphql.py @@ -0,0 +1,896 @@ +import copy +import numbers +from abc import ABCMeta, abstractproperty, abstractmethod + +import six + +from .exceptions import GraphQlQueryFailed + +FIELD_VALUE = object() + + +def fields_to_dict(fields): + if not fields: + return None + + output = {} + for field in fields: + hierarchy = field.split(".") + last = hierarchy.pop(-1) + value = output + for part in hierarchy: + if value is FIELD_VALUE: + break + + if part not in value: + value[part] = {} + value = value[part] + + if value is not FIELD_VALUE: + value[last] = FIELD_VALUE + return output + + +class QueryVariable(object): + """Object representing single varible used in GraphQlQuery. + + Variable definition is in GraphQl query header but it's value is used + in fields. + + Args: + variable_name (str): Name of variable in query. + """ + + def __init__(self, variable_name): + self._variable_name = variable_name + self._name = "${}".format(variable_name) + + @property + def name(self): + """Name used in field filter.""" + + return self._name + + @property + def variable_name(self): + """Name of variable in query definition.""" + + return self._variable_name + + def __hash__(self): + return self._name.__hash__() + + def __str__(self): + return self._name + + def __format__(self, *args, **kwargs): + return self._name.__format__(*args, **kwargs) + + +class GraphQlQuery: + """GraphQl query which can have fields to query. + + Single use object which can be used only for one query. Object and children + objects keep track about paging and progress. + + Args: + name (str): Name of query. + """ + + offset = 2 + + def __init__(self, name): + self._name = name + self._variables = {} + self._children = [] + self._has_multiple_edge_fields = None + + @property + def indent(self): + """Indentation for preparation of query string. + + Returns: + int: Ident spaces. + """ + + return 0 + + @property + def child_indent(self): + """Indentation for preparation of query string used by children. + + Returns: + int: Ident spaces for children. + """ + + return self.indent + + @property + def need_query(self): + """Still need query from server. + + Needed for edges which use pagination. + + Returns: + bool: If still need query from server. + """ + + for child in self._children: + if child.need_query: + return True + return False + + @property + def has_multiple_edge_fields(self): + if self._has_multiple_edge_fields is None: + edge_counter = 0 + for child in self._children: + edge_counter += child.sum_edge_fields(2) + if edge_counter > 1: + break + self._has_multiple_edge_fields = edge_counter > 1 + + return self._has_multiple_edge_fields + + def add_variable(self, key, value_type, value=None): + """Add variable to query. + + Args: + key (str): Variable name. + value_type (str): Type of expected value in variables. This is + graphql type e.g. "[String!]", "Int", "Boolean", etc. + value (Any): Default value for variable. Can be changed later. + + Returns: + QueryVariable: Created variable object. + + Raises: + KeyError: If variable was already added before. + """ + + if key in self._variables: + raise KeyError( + "Variable \"{}\" was already set with type {}.".format( + key, value_type + ) + ) + + variable = QueryVariable(key) + self._variables[key] = { + "type": value_type, + "variable": variable, + "value": value + } + return variable + + def get_variable(self, key): + """Variable object. + + Args: + key (str): Variable name added to headers. + + Returns: + QueryVariable: Variable object used in query string. + """ + + return self._variables[key]["variable"] + + def get_variable_value(self, key, default=None): + """Get Current value of variable. + + Args: + key (str): Variable name. + default (Any): Default value if variable is available. + + Returns: + Any: Variable value. + """ + + variable_item = self._variables.get(key) + if variable_item: + return variable_item["value"] + return default + + def set_variable_value(self, key, value): + """Set value for variable. + + Args: + key (str): Variable name under which the value is stored. + value (Any): Variable value used in query. Variable is not used + if value is 'None'. + """ + + self._variables[key]["value"] = value + + def get_variables_values(self): + """Calculate variable values used that should be used in query. + + Variables with value set to 'None' are skipped. + + Returns: + Dict[str, Any]: Variable values by their name. + """ + + output = {} + for key, item in self._variables.items(): + value = item["value"] + if value is not None: + output[key] = item["value"] + + return output + + def add_obj_field(self, field): + """Add field object to children. + + Args: + field (BaseGraphQlQueryField): Add field to query children. + """ + + if field in self._children: + return + + self._children.append(field) + field.set_parent(self) + + def add_field(self, name, has_edges=None): + """Add field to query. + + Args: + name (str): Field name e.g. 'id'. + has_edges (bool): Field has edges so it need paging. + + Returns: + BaseGraphQlQueryField: Created field object. + """ + + if has_edges: + item = GraphQlQueryEdgeField(name, self) + else: + item = GraphQlQueryField(name, self) + self.add_obj_field(item) + return item + + def calculate_query(self): + """Calculate query string which is sent to server. + + Returns: + str: GraphQl string with variables and headers. + + Raises: + ValueError: Query has no fiels. + """ + + if not self._children: + raise ValueError("Missing fields to query") + + variables = [] + for item in self._variables.values(): + if item["value"] is None: + continue + + variables.append( + "{}: {}".format(item["variable"], item["type"]) + ) + + variables_str = "" + if variables: + variables_str = "({})".format(",".join(variables)) + header = "query {}{}".format(self._name, variables_str) + + output = [] + output.append(header + " {") + for field in self._children: + output.append(field.calculate_query()) + output.append("}") + + return "\n".join(output) + + def parse_result(self, data, output, progress_data): + """Parse data from response for output. + + Output is stored to passed 'output' variable. That's because of paging + during which objects must have access to both new and previous values. + + Args: + data (Dict[str, Any]): Data received using calculated query. + output (Dict[str, Any]): Where parsed data are stored. + """ + + if not data: + return + + for child in self._children: + child.parse_result(data, output, progress_data) + + def query(self, con): + """Do a query from server. + + Args: + con (ServerAPI): Connection to server with 'query' method. + + Returns: + Dict[str, Any]: Parsed output from GraphQl query. + """ + + progress_data = {} + output = {} + while self.need_query: + query_str = self.calculate_query() + variables = self.get_variables_values() + response = con.query_graphql( + query_str, + self.get_variables_values() + ) + if response.errors: + raise GraphQlQueryFailed(response.errors, query_str, variables) + self.parse_result(response.data["data"], output, progress_data) + + return output + + def continuous_query(self, con): + """Do a query from server. + + Args: + con (ServerAPI): Connection to server with 'query' method. + + Returns: + Dict[str, Any]: Parsed output from GraphQl query. + """ + + progress_data = {} + if self.has_multiple_edge_fields: + output = {} + while self.need_query: + query_str = self.calculate_query() + variables = self.get_variables_values() + response = con.query_graphql(query_str, variables) + if response.errors: + raise GraphQlQueryFailed( + response.errors, query_str, variables + ) + self.parse_result(response.data["data"], output, progress_data) + + yield output + + else: + while self.need_query: + output = {} + query_str = self.calculate_query() + variables = self.get_variables_values() + response = con.query_graphql(query_str, variables) + if response.errors: + raise GraphQlQueryFailed( + response.errors, query_str, variables + ) + + self.parse_result(response.data["data"], output, progress_data) + + yield output + + +@six.add_metaclass(ABCMeta) +class BaseGraphQlQueryField(object): + """Field in GraphQl query. + + Args: + name (str): Name of field. + parent (Union[BaseGraphQlQueryField, GraphQlQuery]): Parent object of a + field. + has_edges (bool): Field has edges and should handle paging. + """ + + def __init__(self, name, parent): + if isinstance(parent, GraphQlQuery): + query_item = parent + else: + query_item = parent.query_item + + self._name = name + self._parent = parent + + self._filters = {} + + self._children = [] + # Value is changed on first parse of result + self._need_query = True + + self._query_item = query_item + + self._path = None + + def __repr__(self): + return "<{} {}>".format(self.__class__.__name__, self.path) + + @property + def need_query(self): + """Still need query from server. + + Needed for edges which use pagination. Look into children values too. + + Returns: + bool: If still need query from server. + """ + + if self._need_query: + return True + + for child in self._children: + if child.need_query: + return True + return False + + def sum_edge_fields(self, max_limit=None): + """Check how many edge fields query has. + + In case there are multiple edge fields or are nested the query can't + yield mid cursor results. + + Args: + max_limit (int): Skip rest of counting if counter is bigger then + entered number. + + Returns: + int: Counter edge fields + """ + + counter = 0 + if isinstance(self, GraphQlQueryEdgeField): + counter = 1 + + for child in self._children: + counter += child.sum_edge_fields(max_limit) + if max_limit is not None and counter >= max_limit: + break + return counter + + @property + def offset(self): + return self._query_item.offset + + @property + def indent(self): + return self._parent.child_indent + self.offset + + @abstractproperty + def child_indent(self): + pass + + @property + def query_item(self): + return self._query_item + + @abstractproperty + def has_edges(self): + pass + + @property + def child_has_edges(self): + for child in self._children: + if child.has_edges or child.child_has_edges: + return True + return False + + @property + def path(self): + """Field path for debugging purposes. + + Returns: + str: Field path in query. + """ + + if self._path is None: + if isinstance(self._parent, GraphQlQuery): + path = self._name + else: + path = "/".join((self._parent.path, self._name)) + self._path = path + return self._path + + def reset_cursor(self): + for child in self._children: + child.reset_cursor() + + def get_variable_value(self, *args, **kwargs): + return self._query_item.get_variable_value(*args, **kwargs) + + def set_variable_value(self, *args, **kwargs): + return self._query_item.set_variable_value(*args, **kwargs) + + def set_filter(self, key, value): + self._filters[key] = value + + def has_filter(self, key): + return key in self._filters + + def remove_filter(self, key): + self._filters.pop(key, None) + + def set_parent(self, parent): + if self._parent is parent: + return + self._parent = parent + parent.add_obj_field(self) + + def add_obj_field(self, field): + if field in self._children: + return + + self._children.append(field) + field.set_parent(self) + + def add_field(self, name, has_edges=None): + if has_edges: + item = GraphQlQueryEdgeField(name, self) + else: + item = GraphQlQueryField(name, self) + self.add_obj_field(item) + return item + + def _filter_value_to_str(self, value): + if isinstance(value, QueryVariable): + if self.get_variable_value(value.variable_name) is None: + return None + return str(value) + + if isinstance(value, numbers.Number): + return str(value) + + if isinstance(value, six.string_types): + return '"{}"'.format(value) + + if isinstance(value, (list, set, tuple)): + return "[{}]".format( + ", ".join( + self._filter_value_to_str(item) + for item in iter(value) + ) + ) + raise TypeError( + "Unknown type to convert '{}'".format(str(type(value))) + ) + + def get_filters(self): + """Receive filters for item. + + By default just use copy of set filters. + + Returns: + Dict[str, Any]: Fields filters. + """ + + return copy.deepcopy(self._filters) + + def _filters_to_string(self): + filters = self.get_filters() + if not filters: + return "" + + filter_items = [] + for key, value in filters.items(): + string_value = self._filter_value_to_str(value) + if string_value is None: + continue + + filter_items.append("{}: {}".format(key, string_value)) + + if not filter_items: + return "" + return "({})".format(", ".join(filter_items)) + + def _fake_children_parse(self): + """Mark children as they don't need query.""" + + for child in self._children: + child.parse_result({}, {}, {}) + + @abstractmethod + def calculate_query(self): + pass + + @abstractmethod + def parse_result(self, data, output, progress_data): + pass + + +class GraphQlQueryField(BaseGraphQlQueryField): + has_edges = False + + @property + def child_indent(self): + return self.indent + + def parse_result(self, data, output, progress_data): + if not isinstance(data, dict): + raise TypeError("{} Expected 'dict' type got '{}'".format( + self._name, str(type(data)) + )) + + self._need_query = False + value = data.get(self._name) + if value is None: + self._fake_children_parse() + if self._name in data: + output[self._name] = None + return + + if not self._children: + output[self._name] = value + return + + output_value = output.get(self._name) + if isinstance(value, dict): + if output_value is None: + output_value = {} + output[self._name] = output_value + + for child in self._children: + child.parse_result(value, output_value, progress_data) + return + + if output_value is None: + output_value = [] + output[self._name] = output_value + + if not value: + self._fake_children_parse() + return + + diff = len(value) - len(output_value) + if diff > 0: + for _ in range(diff): + output_value.append({}) + + for idx, item in enumerate(value): + item_value = output_value[idx] + for child in self._children: + child.parse_result(item, item_value, progress_data) + + def calculate_query(self): + offset = self.indent * " " + header = "{}{}{}".format( + offset, + self._name, + self._filters_to_string() + ) + if not self._children: + return header + + output = [] + output.append(header + " {") + + output.extend([ + field.calculate_query() + for field in self._children + ]) + output.append(offset + "}") + + return "\n".join(output) + + +class GraphQlQueryEdgeField(BaseGraphQlQueryField): + has_edges = True + + def __init__(self, *args, **kwargs): + super(GraphQlQueryEdgeField, self).__init__(*args, **kwargs) + self._cursor = None + + @property + def child_indent(self): + offset = self.offset * 2 + return self.indent + offset + + def reset_cursor(self): + # Reset cursor only for edges + self._cursor = None + self._need_query = True + + super(GraphQlQueryEdgeField, self).reset_cursor() + + def parse_result(self, data, output, progress_data): + if not isinstance(data, dict): + raise TypeError("{} Expected 'dict' type got '{}'".format( + self._name, str(type(data)) + )) + + value = data.get(self._name) + if value is None: + self._fake_children_parse() + self._need_query = False + return + + if self._name in output: + node_values = output[self._name] + else: + node_values = [] + output[self._name] = node_values + + handle_cursors = self.child_has_edges + if handle_cursors: + cursor_key = self._get_cursor_key() + if cursor_key in progress_data: + nodes_by_cursor = progress_data[cursor_key] + else: + nodes_by_cursor = {} + progress_data[cursor_key] = nodes_by_cursor + + page_info = value["pageInfo"] + new_cursor = page_info["endCursor"] + self._need_query = page_info["hasNextPage"] + edges = value["edges"] + # Fake result parse + if not edges: + self._fake_children_parse() + + for edge in edges: + if not handle_cursors: + edge_value = {} + node_values.append(edge_value) + else: + edge_cursor = edge["cursor"] + edge_value = nodes_by_cursor.get(edge_cursor) + if edge_value is None: + edge_value = {} + nodes_by_cursor[edge_cursor] = edge_value + node_values.append(edge_value) + + for child in self._children: + child.parse_result(edge["node"], edge_value, progress_data) + + if not self._need_query: + return + + change_cursor = True + for child in self._children: + if child.need_query: + change_cursor = False + + if change_cursor: + for child in self._children: + child.reset_cursor() + self._cursor = new_cursor + + def _get_cursor_key(self): + return "{}/__cursor__".format(self.path) + + def get_filters(self): + filters = super(GraphQlQueryEdgeField, self).get_filters() + + filters["first"] = 300 + if self._cursor: + filters["after"] = self._cursor + return filters + + def calculate_query(self): + if not self._children: + raise ValueError("Missing child definitions for edges {}".format( + self.path + )) + + offset = self.indent * " " + header = "{}{}{}".format( + offset, + self._name, + self._filters_to_string() + ) + + output = [] + output.append(header + " {") + + edges_offset = offset + self.offset * " " + node_offset = edges_offset + self.offset * " " + output.append(edges_offset + "edges {") + output.append(node_offset + "node {") + + for field in self._children: + output.append( + field.calculate_query() + ) + + output.append(node_offset + "}") + if self.child_has_edges: + output.append(node_offset + "cursor") + output.append(edges_offset + "}") + + # Add page information + output.append(edges_offset + "pageInfo {") + for page_key in ( + "endCursor", + "hasNextPage", + ): + output.append(node_offset + page_key) + output.append(edges_offset + "}") + output.append(offset + "}") + + return "\n".join(output) + + +INTROSPECTION_QUERY = """ + query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } + } + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + } + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + } +""" diff --git a/openpype/vendor/python/ayon/ayon_api/graphql_queries.py b/openpype/vendor/python/ayon/ayon_api/graphql_queries.py new file mode 100644 index 0000000000..b6d5c5fcb3 --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/graphql_queries.py @@ -0,0 +1,362 @@ +import collections + +from .graphql import FIELD_VALUE, GraphQlQuery + + +def fields_to_dict(fields): + if not fields: + return None + + output = {} + for field in fields: + hierarchy = field.split(".") + last = hierarchy.pop(-1) + value = output + for part in hierarchy: + if value is FIELD_VALUE: + break + + if part not in value: + value[part] = {} + value = value[part] + + if value is not FIELD_VALUE: + value[last] = FIELD_VALUE + return output + + +def project_graphql_query(fields): + query = GraphQlQuery("ProjectQuery") + project_name_var = query.add_variable("projectName", "String!") + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + nested_fields = fields_to_dict(fields) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, project_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 + + +def projects_graphql_query(fields): + query = GraphQlQuery("ProjectsQuery") + projects_field = query.add_field("projects", has_edges=True) + + nested_fields = fields_to_dict(fields) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, projects_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 + + +def folders_graphql_query(fields): + query = GraphQlQuery("FoldersQuery") + project_name_var = query.add_variable("projectName", "String!") + folder_ids_var = query.add_variable("folderIds", "[String!]") + parent_folder_ids_var = query.add_variable("parentFolderIds", "[String!]") + folder_paths_var = query.add_variable("folderPaths", "[String!]") + folder_names_var = query.add_variable("folderNames", "[String!]") + has_subsets_var = query.add_variable("folderHasSubsets", "Boolean!") + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + folders_field = project_field.add_field("folders", has_edges=True) + folders_field.set_filter("ids", folder_ids_var) + folders_field.set_filter("parentIds", parent_folder_ids_var) + folders_field.set_filter("names", folder_names_var) + folders_field.set_filter("paths", folder_paths_var) + folders_field.set_filter("hasSubsets", has_subsets_var) + + nested_fields = fields_to_dict(fields) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, folders_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 + + +def tasks_graphql_query(fields): + query = GraphQlQuery("TasksQuery") + project_name_var = query.add_variable("projectName", "String!") + task_ids_var = query.add_variable("taskIds", "[String!]") + task_names_var = query.add_variable("taskNames", "[String!]") + task_types_var = query.add_variable("taskTypes", "[String!]") + folder_ids_var = query.add_variable("folderIds", "[String!]") + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + tasks_field = project_field.add_field("tasks", has_edges=True) + tasks_field.set_filter("ids", task_ids_var) + # WARNING: At moment when this been created 'names' filter is not supported + tasks_field.set_filter("names", task_names_var) + tasks_field.set_filter("taskTypes", task_types_var) + tasks_field.set_filter("folderIds", folder_ids_var) + + nested_fields = fields_to_dict(fields) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, tasks_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 + + +def subsets_graphql_query(fields): + query = GraphQlQuery("SubsetsQuery") + + project_name_var = query.add_variable("projectName", "String!") + folder_ids_var = query.add_variable("folderIds", "[String!]") + subset_ids_var = query.add_variable("subsetIds", "[String!]") + subset_names_var = query.add_variable("subsetNames", "[String!]") + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + subsets_field = project_field.add_field("subsets", has_edges=True) + subsets_field.set_filter("ids", subset_ids_var) + subsets_field.set_filter("names", subset_names_var) + subsets_field.set_filter("folderIds", folder_ids_var) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, subsets_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 + + +def versions_graphql_query(fields): + query = GraphQlQuery("VersionsQuery") + + project_name_var = query.add_variable("projectName", "String!") + subset_ids_var = query.add_variable("subsetIds", "[String!]") + version_ids_var = query.add_variable("versionIds", "[String!]") + versions_var = query.add_variable("versions", "[Int!]") + hero_only_var = query.add_variable("heroOnly", "Boolean") + latest_only_var = query.add_variable("latestOnly", "Boolean") + hero_or_latest_only_var = query.add_variable( + "heroOrLatestOnly", "Boolean" + ) + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + subsets_field = project_field.add_field("versions", has_edges=True) + subsets_field.set_filter("ids", version_ids_var) + subsets_field.set_filter("subsetIds", subset_ids_var) + subsets_field.set_filter("versions", versions_var) + subsets_field.set_filter("heroOnly", hero_only_var) + subsets_field.set_filter("latestOnly", latest_only_var) + subsets_field.set_filter("heroOrLatestOnly", hero_or_latest_only_var) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, subsets_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 + + +def representations_graphql_query(fields): + query = GraphQlQuery("RepresentationsQuery") + + project_name_var = query.add_variable("projectName", "String!") + repre_ids_var = query.add_variable("representationIds", "[String!]") + repre_names_var = query.add_variable("representationNames", "[String!]") + version_ids_var = query.add_variable("versionIds", "[String!]") + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + repres_field = project_field.add_field("representations", has_edges=True) + repres_field.set_filter("ids", repre_ids_var) + repres_field.set_filter("versionIds", version_ids_var) + repres_field.set_filter("names", repre_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, repres_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 + + +def representations_parents_qraphql_query( + version_fields, subset_fields, folder_fields +): + + query = GraphQlQuery("RepresentationsParentsQuery") + + project_name_var = query.add_variable("projectName", "String!") + repre_ids_var = query.add_variable("representationIds", "[String!]") + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + repres_field = project_field.add_field("representations", has_edges=True) + repres_field.add_field("id") + repres_field.set_filter("ids", repre_ids_var) + version_field = repres_field.add_field("version") + + fields_queue = collections.deque() + for key, value in fields_to_dict(version_fields).items(): + fields_queue.append((key, value, version_field)) + + subset_field = version_field.add_field("subset") + for key, value in fields_to_dict(subset_fields).items(): + fields_queue.append((key, value, subset_field)) + + folder_field = subset_field.add_field("folder") + for key, value in fields_to_dict(folder_fields).items(): + fields_queue.append((key, value, folder_field)) + + while fields_queue: + item = fields_queue.popleft() + key, value, parent = item + field = parent.add_field(key) + if value is FIELD_VALUE: + continue + + for k, v in value.items(): + fields_queue.append((k, v, field)) + + return query + + +def workfiles_info_graphql_query(fields): + query = GraphQlQuery("WorkfilesInfo") + project_name_var = query.add_variable("projectName", "String!") + workfiles_info_ids = query.add_variable("workfileIds", "[String!]") + task_ids_var = query.add_variable("taskIds", "[String!]") + paths_var = query.add_variable("paths", "[String!]") + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + workfiles_field = project_field.add_field("workfiles", has_edges=True) + workfiles_field.set_filter("ids", workfiles_info_ids) + workfiles_field.set_filter("taskIds", task_ids_var) + workfiles_field.set_filter("paths", paths_var) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, workfiles_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 + + +def events_graphql_query(fields): + query = GraphQlQuery("WorkfilesInfo") + topics_var = query.add_variable("eventTopics", "[String!]") + projects_var = query.add_variable("projectNames", "[String!]") + states_var = query.add_variable("eventStates", "[String!]") + users_var = query.add_variable("eventUsers", "[String!]") + include_logs_var = query.add_variable("includeLogsFilter", "Boolean!") + + events_field = query.add_field("events", has_edges=True) + events_field.set_filter("topics", topics_var) + events_field.set_filter("projects", projects_var) + events_field.set_filter("states", states_var) + events_field.set_filter("users", users_var) + events_field.set_filter("includeLogs", include_logs_var) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, events_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 diff --git a/openpype/vendor/python/ayon/ayon_api/operations.py b/openpype/vendor/python/ayon/ayon_api/operations.py new file mode 100644 index 0000000000..21adc229d2 --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/operations.py @@ -0,0 +1,688 @@ +import copy +import collections +import uuid +from abc import ABCMeta, abstractproperty + +import six + +from ._api import get_server_api_connection +from .utils import create_entity_id, REMOVED_VALUE + + +def _create_or_convert_to_id(entity_id=None): + if entity_id is None: + return create_entity_id() + + # Validate if can be converted to uuid + uuid.UUID(entity_id) + return entity_id + + +def new_folder_entity( + name, + folder_type, + parent_id=None, + attribs=None, + data=None, + thumbnail_id=None, + entity_id=None +): + """Create skeleton data of folder entity. + + Args: + name (str): Is considered as unique identifier of folder in project. + parent_id (str): Id of parent folder. + attribs (Dict[str, Any]): Explicitly set attributes of folder. + data (Dict[str, Any]): Custom folder data. Empty dictionary is used + if not passed. + thumbnail_id (str): Id of thumbnail related to folder. + entity_id (str): Predefined id of entity. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of folder entity. + """ + + if attribs is None: + attribs = {} + + if data is None: + data = {} + + if parent_id is not None: + parent_id = _create_or_convert_to_id(parent_id) + + return { + "id": _create_or_convert_to_id(entity_id), + "name": name, + # This will be ignored + "folderType": folder_type, + "parentId": parent_id, + "data": data, + "attrib": attribs, + "thumbnailId": thumbnail_id + } + + +def new_subset_entity( + name, family, folder_id, attribs=None, data=None, entity_id=None +): + """Create skeleton data of subset entity. + + Args: + name (str): Is considered as unique identifier of subset under folder. + family (str): Subset's family. + folder_id (str): Id of parent folder. + attribs (Dict[str, Any]): Explicitly set attributes of subset. + data (Dict[str, Any]): Subset entity data. Empty dictionary is used + if not passed. Value of 'family' is used to fill 'family'. + entity_id (str): Predefined id of entity. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of subset entity. + """ + + if attribs is None: + attribs = {} + + if data is None: + data = {} + + return { + "id": _create_or_convert_to_id(entity_id), + "name": name, + "family": family, + "attrib": attribs, + "data": data, + "folderId": _create_or_convert_to_id(folder_id) + } + + +def new_version_entity( + version, + subset_id, + task_id=None, + thumbnail_id=None, + author=None, + attribs=None, + data=None, + entity_id=None +): + """Create skeleton data of version entity. + + Args: + version (int): Is considered as unique identifier of version + under subset. + subset_id (str): Id of parent subset. + task_id (str): Id of task under which subset was created. + thumbnail_id (str): Thumbnail related to version. + author (str): Name of version author. + attribs (Dict[str, Any]): Explicitly set attributes of version. + data (Dict[str, Any]): Version entity custom data. + entity_id (str): Predefined id of entity. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version entity. + """ + + if attribs is None: + attribs = {} + + if data is None: + data = {} + + if data is None: + data = {} + + output = { + "id": _create_or_convert_to_id(entity_id), + "version": int(version), + "subsetId": _create_or_convert_to_id(subset_id), + "attrib": attribs, + "data": data + } + if task_id: + output["taskId"] = task_id + if thumbnail_id: + output["thumbnailId"] = thumbnail_id + if author: + output["author"] = author + return output + + +def new_hero_version_entity( + version, + subset_id, + task_id=None, + thumbnail_id=None, + author=None, + attribs=None, + data=None, + entity_id=None +): + """Create skeleton data of hero version entity. + + Args: + version (int): Is considered as unique identifier of version + under subset. Should be same as standard version if there is any. + subset_id (str): Id of parent subset. + task_id (str): Id of task under which subset was created. + thumbnail_id (str): Thumbnail related to version. + author (str): Name of version author. + attribs (Dict[str, Any]): Explicitly set attributes of version. + data (Dict[str, Any]): Version entity data. + entity_id (str): Predefined id of entity. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version entity. + """ + + if attribs is None: + attribs = {} + + if data is None: + data = {} + + output = { + "id": _create_or_convert_to_id(entity_id), + "version": -abs(int(version)), + "subsetId": subset_id, + "attrib": attribs, + "data": data + } + if task_id: + output["taskId"] = task_id + if thumbnail_id: + output["thumbnailId"] = thumbnail_id + if author: + output["author"] = author + return output + + +def new_representation_entity( + name, version_id, 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. + attribs (Dict[str, Any]): Explicitly set attributes of representation. + data (Dict[str, Any]): Representation entity data. + entity_id (str): Predefined id of entity. New id is created + if not passed. + + Returns: + Dict[str, Any]: Skeleton of representation entity. + """ + + if attribs is None: + attribs = {} + + if data is None: + data = {} + + return { + "id": _create_or_convert_to_id(entity_id), + "versionId": _create_or_convert_to_id(version_id), + "name": name, + "data": data, + "attrib": attribs + } + + +def new_workfile_info_doc( + filename, folder_id, task_name, files, 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. + data (Dict[str, Any]): Additional metadata. + entity_id (str): Predefined id of entity. New id is created + if not passed. + + Returns: + Dict[str, Any]: Skeleton of workfile info entity. + """ + + if not data: + data = {} + + return { + "id": _create_or_convert_to_id(entity_id), + "parent": _create_or_convert_to_id(folder_id), + "task_name": task_name, + "filename": filename, + "data": data, + "files": files + } + + +@six.add_metaclass(ABCMeta) +class AbstractOperation(object): + """Base operation class. + + Opration represent a call into database. The call can create, change or + remove data. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'folder', 'representation' etc. + """ + + def __init__(self, project_name, entity_type, session): + self._project_name = project_name + self._entity_type = entity_type + self._session = session + self._id = str(uuid.uuid4()) + + @property + def project_name(self): + return self._project_name + + @property + def id(self): + """Identifier of operation.""" + + return self._id + + @property + def entity_type(self): + return self._entity_type + + @abstractproperty + def operation_name(self): + """Stringified type of operation.""" + + pass + + def to_data(self): + """Convert opration to data that can be converted to json or others. + + Returns: + Dict[str, Any]: Description of operation. + """ + + return { + "id": self._id, + "entity_type": self.entity_type, + "project_name": self.project_name, + "operation": self.operation_name + } + + +class CreateOperation(AbstractOperation): + """Opeartion to create an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'folder', 'representation' etc. + data (Dict[str, Any]): Data of entity that will be created. + """ + + operation_name = "create" + + def __init__(self, project_name, entity_type, data, session): + if not data: + data = {} + else: + data = copy.deepcopy(dict(data)) + + if "id" not in data: + data["id"] = create_entity_id() + + self._data = data + super(CreateOperation, self).__init__( + project_name, entity_type, session + ) + + def __setitem__(self, key, value): + self.set_value(key, value) + + def __getitem__(self, key): + return self.data[key] + + def set_value(self, key, value): + self.data[key] = value + + def get(self, key, *args, **kwargs): + return self.data.get(key, *args, **kwargs) + + @property + def con(self): + return self.session.con + + @property + def session(self): + return self._session + + @property + def entity_id(self): + return self._data["id"] + + @property + def data(self): + return self._data + + def to_data(self): + output = super(CreateOperation, self).to_data() + output["data"] = copy.deepcopy(self.data) + return output + + def to_server_operation(self): + return { + "id": self.id, + "type": "create", + "entityType": self.entity_type, + "entityId": self.entity_id, + "data": self._data + } + + +class UpdateOperation(AbstractOperation): + """Operation to update an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'folder', 'representation' etc. + entity_id (str): Identifier of an entity. + update_data (Dict[str, Any]): Key -> value changes that will be set in + database. If value is set to 'REMOVED_VALUE' the key will be + removed. Only first level of dictionary is checked (on purpose). + """ + + operation_name = "update" + + def __init__( + self, project_name, entity_type, entity_id, update_data, session + ): + super(UpdateOperation, self).__init__( + project_name, entity_type, session + ) + + self._entity_id = entity_id + self._update_data = update_data + + @property + def entity_id(self): + return self._entity_id + + @property + def update_data(self): + return self._update_data + + @property + def con(self): + return self.session.con + + @property + def session(self): + return self._session + + def to_data(self): + changes = {} + for key, value in self._update_data.items(): + if value is REMOVED_VALUE: + value = None + changes[key] = value + + output = super(UpdateOperation, self).to_data() + output.update({ + "entity_id": self.entity_id, + "changes": changes + }) + return output + + def to_server_operation(self): + if not self._update_data: + return None + + update_data = {} + for key, value in self._update_data.items(): + if value is REMOVED_VALUE: + value = None + update_data[key] = value + + return { + "id": self.id, + "type": "update", + "entityType": self.entity_type, + "entityId": self.entity_id, + "data": update_data + } + + +class DeleteOperation(AbstractOperation): + """Opeartion to delete an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'folder', 'representation' etc. + entity_id (str): Entity id that will be removed. + """ + + operation_name = "delete" + + def __init__(self, project_name, entity_type, entity_id, session): + self._entity_id = entity_id + + super(DeleteOperation, self).__init__( + project_name, entity_type, session + ) + + @property + def entity_id(self): + return self._entity_id + + @property + def con(self): + return self.session.con + + @property + def session(self): + return self._session + + def to_data(self): + output = super(DeleteOperation, self).to_data() + output["entity_id"] = self.entity_id + return output + + def to_server_operation(self): + return { + "id": self.id, + "type": self.operation_name, + "entityId": self.entity_id, + "entityType": self.entity_type, + } + + +class OperationsSession(object): + """Session storing operations that should happen in an order. + + At this moment does not handle anything special can be sonsidered as + stupid list of operations that will happen after each other. If creation + of same entity is there multiple times it's handled in any way and entity + values are not validated. + + All operations must be related to single project. + + Args: + project_name (str): Project name to which are operations related. + """ + + def __init__(self, con=None): + if con is None: + con = get_server_api_connection() + self._con = con + self._project_cache = {} + self._operations = [] + self._nested_operations = collections.defaultdict(list) + + @property + def con(self): + return self._con + + def get_project(self, project_name): + if project_name not in self._project_cache: + self._project_cache[project_name] = self.con.get_project( + project_name) + return copy.deepcopy(self._project_cache[project_name]) + + def __len__(self): + return len(self._operations) + + def add(self, operation): + """Add operation to be processed. + + Args: + operation (BaseOperation): Operation that should be processed. + """ + if not isinstance( + operation, + (CreateOperation, UpdateOperation, DeleteOperation) + ): + raise TypeError("Expected Operation object got {}".format( + str(type(operation)) + )) + + self._operations.append(operation) + + def append(self, operation): + """Add operation to be processed. + + Args: + operation (BaseOperation): Operation that should be processed. + """ + + self.add(operation) + + def extend(self, operations): + """Add operations to be processed. + + Args: + operations (List[BaseOperation]): Operations that should be + processed. + """ + + for operation in operations: + self.add(operation) + + def remove(self, operation): + """Remove operation.""" + + self._operations.remove(operation) + + def clear(self): + """Clear all registered operations.""" + + self._operations = [] + + def to_data(self): + return [ + operation.to_data() + for operation in self._operations + ] + + def commit(self): + """Commit session operations.""" + + operations, self._operations = self._operations, [] + if not operations: + return + + operations_by_project = collections.defaultdict(list) + for operation in operations: + operations_by_project[operation.project_name].append(operation) + + for project_name, operations in operations_by_project.items(): + operations_body = [] + for operation in operations: + body = operation.to_server_operation() + if body is not None: + operations_body.append(body) + + self._con.send_batch_operations( + project_name, operations_body, can_fail=False + ) + + def create_entity(self, project_name, entity_type, data, nested_id=None): + """Fast access to 'CreateOperation'. + + Args: + project_name (str): On which project the creation happens. + entity_type (str): Which entity type will be created. + data (Dicst[str, Any]): Entity data. + nested_id (str): Id of other operation from which is triggered + operation -> Operations can trigger suboperations but they + must be added to operations list after it's parent is added. + + Returns: + CreateOperation: Object of update operation. + """ + + operation = CreateOperation( + project_name, entity_type, data, self + ) + + if nested_id: + self._nested_operations[nested_id].append(operation) + else: + self.add(operation) + if operation.id in self._nested_operations: + self.extend(self._nested_operations.pop(operation.id)) + + return operation + + def update_entity( + self, project_name, entity_type, entity_id, update_data, nested_id=None + ): + """Fast access to 'UpdateOperation'. + + Returns: + UpdateOperation: Object of update operation. + """ + + operation = UpdateOperation( + project_name, entity_type, entity_id, update_data, self + ) + if nested_id: + self._nested_operations[nested_id].append(operation) + else: + self.add(operation) + if operation.id in self._nested_operations: + self.extend(self._nested_operations.pop(operation.id)) + return operation + + def delete_entity( + self, project_name, entity_type, entity_id, nested_id=None + ): + """Fast access to 'DeleteOperation'. + + Returns: + DeleteOperation: Object of delete operation. + """ + + operation = DeleteOperation( + project_name, entity_type, entity_id, self + ) + if nested_id: + self._nested_operations[nested_id].append(operation) + else: + self.add(operation) + if operation.id in self._nested_operations: + self.extend(self._nested_operations.pop(operation.id)) + return operation diff --git a/openpype/vendor/python/ayon/ayon_api/server_api.py b/openpype/vendor/python/ayon/ayon_api/server_api.py new file mode 100644 index 0000000000..e3a42e4dad --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/server_api.py @@ -0,0 +1,4247 @@ +import os +import re +import io +import json +import logging +import collections +import platform +import copy +import uuid +from contextlib import contextmanager +try: + from http import HTTPStatus +except ImportError: + HTTPStatus = None + +import requests + +from .constants import ( + DEFAULT_PROJECT_FIELDS, + DEFAULT_FOLDER_FIELDS, + DEFAULT_TASK_FIELDS, + DEFAULT_SUBSET_FIELDS, + DEFAULT_VERSION_FIELDS, + DEFAULT_REPRESENTATION_FIELDS, + REPRESENTATION_FILES_FIELDS, + DEFAULT_WORKFILE_INFO_FIELDS, + DEFAULT_EVENT_FIELDS, +) +from .thumbnails import ThumbnailCache +from .graphql import GraphQlQuery, INTROSPECTION_QUERY +from .graphql_queries import ( + project_graphql_query, + projects_graphql_query, + folders_graphql_query, + tasks_graphql_query, + subsets_graphql_query, + versions_graphql_query, + representations_graphql_query, + representations_parents_qraphql_query, + workfiles_info_graphql_query, + events_graphql_query, +) +from .exceptions import ( + FailedOperations, + UnauthorizedError, + AuthenticationError, + ServerNotReached, + ServerError, +) +from .utils import ( + RepresentationParents, + prepare_query_string, + logout_from_server, + create_entity_id, + entity_data_json_default, + failed_json_default, + TransferProgress, +) + +PatternType = type(re.compile("")) +JSONDecodeError = getattr(json, "JSONDecodeError", ValueError) +# This should be collected from server schema +PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" +PROJECT_NAME_REGEX = re.compile( + "^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS) +) + + +def _get_description(response): + if HTTPStatus is None: + return str(response.orig_response) + return HTTPStatus(response.status).description + + +class RequestType: + def __init__(self, name): + self.name = name + + def __hash__(self): + return self.name.__hash__() + + +class RequestTypes: + get = RequestType("GET") + post = RequestType("POST") + put = RequestType("PUT") + patch = RequestType("PATCH") + delete = RequestType("DELETE") + + +class RestApiResponse(object): + """API Response.""" + + def __init__(self, response, data=None): + if response is None: + status_code = 500 + else: + status_code = response.status_code + self._response = response + self.status = status_code + self._data = data + + @property + def orig_response(self): + return self._response + + @property + def headers(self): + return self._response.headers + + @property + def data(self): + if self._data is None: + if self.status != 204: + self._data = self.orig_response.json() + else: + self._data = {} + return self._data + + @property + def content(self): + return self._response.content + + @property + def content_type(self): + return self.headers.get("Content-Type") + + @property + def detail(self): + return self.get("detail", _get_description(self)) + + @property + def status_code(self): + return self.status + + def raise_for_status(self): + self._response.raise_for_status() + + def __enter__(self, *args, **kwargs): + return self._response.__enter__(*args, **kwargs) + + def __contains__(self, key): + return key in self.data + + def __repr__(self): + return "<{}: {} ({})>".format( + self.__class__.__name__, self.status, self.detail + ) + + def __len__(self): + return 200 <= self.status < 400 + + def __getitem__(self, key): + return self.data[key] + + def get(self, key, default=None): + data = self.data + if isinstance(data, dict): + return self.data.get(key, default) + return default + + +class GraphQlResponse: + def __init__(self, data): + self.data = data + self.errors = data.get("errors") + + def __len__(self): + if self.errors: + return 0 + return 1 + + def __repr__(self): + if self.errors: + return "<{} errors={}>".format( + self.__class__.__name__, self.errors[0]['message'] + ) + return "<{}>".format(self.__class__.__name__) + + +def fill_own_attribs(entity): + if not entity or not entity.get("attrib"): + return + + attributes = set(entity["ownAttrib"]) + + own_attrib = {} + entity["ownAttrib"] = own_attrib + + for key, value in entity["attrib"].items(): + if key not in attributes: + own_attrib[key] = None + else: + own_attrib[key] = copy.deepcopy(value) + + +class _AsUserStack: + """Handle stack of users used over server api connection in service mode. + + ServerAPI can behave as other users if it is using special API key. + + Examples: + >>> stack = _AsUserStack() + >>> stack.set_default_username("DefaultName") + >>> print(stack.username) + DefaultName + >>> with stack.as_user("Other1"): + ... print(stack.username) + ... with stack.as_user("Other2"): + ... print(stack.username) + ... print(stack.username) + ... stack.clear() + ... print(stack.username) + Other1 + Other2 + Other1 + None + >>> print(stack.username) + None + >>> stack.set_default_username("DefaultName") + >>> print(stack.username) + DefaultName + """ + + def __init__(self): + self._users_by_id = {} + self._user_ids = [] + self._last_user = None + self._default_user = None + + def clear(self): + self._users_by_id = {} + self._user_ids = [] + self._last_user = None + self._default_user = None + + @property + def username(self): + # Use '_user_ids' for boolean check to have ability "unset" + # default user + if self._user_ids: + return self._last_user + return self._default_user + + def get_default_username(self): + return self._default_user + + def set_default_username(self, username=None): + self._default_user = username + + default_username = property(get_default_username, set_default_username) + + @contextmanager + def as_user(self, username): + self._last_user = username + user_id = uuid.uuid4().hex + self._user_ids.append(user_id) + self._users_by_id[user_id] = username + try: + yield + finally: + self._users_by_id.pop(user_id, None) + if not self._user_ids: + return + + # First check if is the user id the last one + was_last = self._user_ids[-1] == user_id + # Remove id from variables + if user_id in self._user_ids: + self._user_ids.remove(user_id) + + if not was_last: + return + + new_last_user = None + if self._user_ids: + new_last_user = self._users_by_id.get(self._user_ids[-1]) + self._last_user = new_last_user + + +class ServerAPI(object): + """Base handler of connection to server. + + Requires url to server which is used as base for api and graphql calls. + + Login cause that a session is used + + Args: + base_url (str): Example: http://localhost:5000 + token (str): Access token (api key) to server. + site_id (str): Unique name of site. Should be the same when + connection is created from the same machine under same user. + client_version (str): Version of client application (used in + desktop client application). + default_settings_variant (Union[str, None]): Settings variant used by + default if a method for settings won't get any (by default is + 'production'). + """ + + def __init__( + self, + base_url, + token=None, + site_id=None, + client_version=None, + default_settings_variant=None + ): + if not base_url: + raise ValueError("Invalid server URL {}".format(str(base_url))) + + base_url = base_url.rstrip("/") + self._base_url = base_url + self._rest_url = "{}/api".format(base_url) + self._graphl_url = "{}/graphql".format(base_url) + self._log = None + self._access_token = token + self._site_id = site_id + self._client_version = client_version + self._default_settings_variant = default_settings_variant + self._access_token_is_service = None + self._token_is_valid = None + self._server_available = None + + self._session = None + + self._base_functions_mapping = { + RequestTypes.get: requests.get, + RequestTypes.post: requests.post, + RequestTypes.put: requests.put, + RequestTypes.patch: requests.patch, + RequestTypes.delete: requests.delete + } + self._session_functions_mapping = {} + + # Attributes cache + self._attributes_schema = None + self._entity_type_attributes_cache = {} + + self._as_user_stack = _AsUserStack() + self._thumbnail_cache = ThumbnailCache(True) + + @property + def log(self): + if self._log is None: + self._log = logging.getLogger(self.__class__.__name__) + return self._log + + def get_base_url(self): + return self._base_url + + def get_rest_url(self): + return self._rest_url + + base_url = property(get_base_url) + rest_url = property(get_rest_url) + + @property + def access_token(self): + """Access token used for authorization to server. + + Returns: + Union[str, None]: Token string or None if not authorized yet. + """ + + return self._access_token + + def get_site_id(self): + """Site id used for connection. + + Site id tells server from which machine/site is connection created and + is used for default site overrides when settings are received. + + Returns: + Union[str, None]: Site id value or None if not filled. + """ + + return self._site_id + + def set_site_id(self, site_id): + """Change site id of connection. + + Behave as specific site for server. It affects default behavior of + settings getter methods. + + Args: + site_id (Union[str, None]): Site id value, or 'None' to unset. + """ + + if self._site_id == site_id: + return + self._site_id = site_id + # Recreate session on machine id change + self._update_session_headers() + + site_id = property(get_site_id, set_site_id) + + def get_client_version(self): + """Version of client used to connect to server. + + Client version is AYON client build desktop application. + + Returns: + str: Client version string used in connection. + """ + + return self._client_version + + def set_client_version(self, client_version): + """Set version of client used to connect to server. + + Client version is AYON client build desktop application. + + Args: + client_version (Union[str, None]): Client version string. + """ + + if self._client_version == client_version: + return + + self._client_version = client_version + self._update_session_headers() + + client_version = property(get_client_version, set_client_version) + + def get_default_settings_variant(self): + """Default variant used for settings. + + Returns: + Union[str, None]: name of variant or None. + """ + + return self._default_settings_variant + + def set_default_settings_variant(self, variant): + """Change default variant for addon settings. + + Note: + It is recommended to set only 'production' or 'staging' variants + as default variant. + + Args: + variant (Union[str, None]): Settings variant name. + """ + + self._default_settings_variant = variant + + default_settings_variant = property( + get_default_settings_variant, + set_default_settings_variant + ) + + def get_default_service_username(self): + """Default username used for callbacks when used with service API key. + + Returns: + Union[str, None]: Username if any was filled. + """ + + return self._as_user_stack.get_default_username() + + def set_default_service_username(self, username=None): + """Service API will work as other user. + + Service API keys can work as other user. It can be temporary using + context manager 'as_user' or it is possible to set default username if + 'as_user' context manager is not entered. + + Args: + username (Union[str, None]): Username to work as when service. + + Raises: + ValueError: When connection is not yet authenticated or api key + is not service token. + """ + + current_username = self._as_user_stack.get_default_username() + if current_username == username: + return + + if not self.has_valid_token: + raise ValueError( + "Authentication of connection did not happen yet." + ) + + if not self._access_token_is_service: + raise ValueError( + "Can't set service username. API key is not a service token." + ) + + self._as_user_stack.set_default_username(username) + if self._as_user_stack.username == username: + self._update_session_headers() + + @contextmanager + def as_username(self, username): + """Service API will temporarily work as other user. + + This method can be used only if service API key is logged in. + + Args: + username (Union[str, None]): Username to work as when service. + + Raises: + ValueError: When connection is not yet authenticated or api key + is not service token. + """ + + if not self.has_valid_token: + raise ValueError( + "Authentication of connection did not happen yet." + ) + + if not self._access_token_is_service: + raise ValueError( + "Can't set service username. API key is not a service token." + ) + + with self._as_user_stack.as_user(username) as o: + self._update_session_headers() + try: + yield o + finally: + self._update_session_headers() + + @property + def is_server_available(self): + if self._server_available is None: + response = requests.get(self._base_url) + self._server_available = response.status_code == 200 + return self._server_available + + @property + def has_valid_token(self): + if self._access_token is None: + return False + + if self._token_is_valid is None: + self.validate_token() + return self._token_is_valid + + def validate_server_availability(self): + if not self.is_server_available: + raise ServerNotReached("Server \"{}\" can't be reached".format( + self._base_url + )) + + def validate_token(self): + try: + # TODO add other possible validations + # - existence of 'user' key in info + # - validate that 'site_id' is in 'sites' in info + self.get_info() + self.get_user() + self._token_is_valid = True + + except UnauthorizedError: + self._token_is_valid = False + return self._token_is_valid + + def set_token(self, token): + self.reset_token() + self._access_token = token + self.get_user() + + def reset_token(self): + self._access_token = None + self._token_is_valid = None + self.close_session() + + def create_session(self): + if self._session is not None: + raise ValueError("Session is already created.") + + self._as_user_stack.clear() + # Validate token before session creation + self.validate_token() + + session = requests.Session() + session.headers.update(self.get_headers()) + + self._session_functions_mapping = { + RequestTypes.get: session.get, + RequestTypes.post: session.post, + RequestTypes.put: session.put, + RequestTypes.patch: session.patch, + RequestTypes.delete: session.delete + } + self._session = session + + def close_session(self): + if self._session is None: + return + + session = self._session + self._session = None + self._session_functions_mapping = {} + session.close() + + def _update_session_headers(self): + if self._session is None: + return + + # Header keys that may change over time + for key, value in ( + ("X-as-user", self._as_user_stack.username), + ("x-ayon-version", self._client_version), + ("x-ayon-site-id", self._site_id), + ): + if value is not None: + self._session.headers[key] = value + elif key in self._session.headers: + self._session.headers.pop(key) + + def get_info(self): + """Get information about current used api key. + + By default, the 'info' contains only 'uptime' and 'version'. With + logged user info also contains information about user and machines on + which was logged in. + + Todos: + Use this method for validation of token instead of 'get_user'. + + Returns: + Dict[str, Any]: Information from server. + """ + + response = self.get("info") + return response.data + + def _get_user_info(self): + if self._access_token is None: + return None + + if self._access_token_is_service is not None: + response = self.get("users/me") + return response.data + + self._access_token_is_service = False + response = self.get("users/me") + if response.status == 200: + return response.data + + self._access_token_is_service = True + response = self.get("users/me") + if response.status == 200: + return response.data + + 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_user(self, username=None): + output = None + if username is None: + output = self._get_user_info() + else: + response = self.get("users/{}".format(username)) + if response.status == 200: + output = response.data + + if output is None: + raise UnauthorizedError("User is not authorized.") + return output + + def get_headers(self, content_type=None): + if content_type is None: + content_type = "application/json" + + headers = { + "Content-Type": content_type, + "x-ayon-platform": platform.system().lower(), + "x-ayon-hostname": platform.node(), + } + if self._site_id is not None: + headers["x-ayon-site-id"] = self._site_id + + if self._client_version is not None: + headers["x-ayon-version"] = self._client_version + + if self._access_token: + if self._access_token_is_service: + headers["X-Api-Key"] = self._access_token + username = self._as_user_stack.username + if username: + headers["X-as-user"] = username + else: + headers["Authorization"] = "Bearer {}".format( + self._access_token) + return headers + + def login(self, username, password): + if self.has_valid_token: + try: + user_info = self.get_user() + except UnauthorizedError: + user_info = {} + + current_username = user_info.get("name") + if current_username == username: + self.close_session() + self.create_session() + return + + self.reset_token() + + 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) + + raise AuthenticationError("Login failed {}".format(details)) + + self._access_token = response["token"] + + if not self.has_valid_token: + raise AuthenticationError("Invalid credentials") + self.create_session() + + def logout(self, soft=False): + if self._access_token: + if not soft: + self._logout() + self.reset_token() + + def _logout(self): + logout_from_server(self._base_url, self._access_token) + + def _do_rest_request(self, function, url, **kwargs): + if self._session is None: + if "headers" not in kwargs: + kwargs["headers"] = self.get_headers() + + if isinstance(function, RequestType): + function = self._base_functions_mapping[function] + + elif isinstance(function, RequestType): + function = self._session_functions_mapping[function] + + try: + response = function(url, **kwargs) + + except ConnectionRefusedError: + new_response = RestApiResponse( + None, + {"detail": "Unable to connect the server. Connection refused"} + ) + except requests.exceptions.ConnectionError: + new_response = RestApiResponse( + None, + {"detail": "Unable to connect the server. Connection error"} + ) + else: + content_type = response.headers.get("Content-Type") + if content_type == "application/json": + try: + new_response = RestApiResponse(response) + except JSONDecodeError: + new_response = RestApiResponse( + None, + { + "detail": "The response is not a JSON: {}".format( + response.text) + } + ) + + elif content_type in ("image/jpeg", "image/png"): + new_response = RestApiResponse(response) + + else: + new_response = RestApiResponse(response) + + self.log.debug("Response {}".format(str(new_response))) + return new_response + + def raw_post(self, entrypoint, **kwargs): + entrypoint = entrypoint.lstrip("/").rstrip("/") + self.log.debug("Executing [POST] {}".format(entrypoint)) + url = "{}/{}".format(self._rest_url, entrypoint) + return self._do_rest_request( + RequestTypes.post, + url, + **kwargs + ) + + def raw_put(self, entrypoint, **kwargs): + entrypoint = entrypoint.lstrip("/").rstrip("/") + self.log.debug("Executing [PUT] {}".format(entrypoint)) + url = "{}/{}".format(self._rest_url, entrypoint) + return self._do_rest_request( + RequestTypes.put, + url, + **kwargs + ) + + def raw_patch(self, entrypoint, **kwargs): + entrypoint = entrypoint.lstrip("/").rstrip("/") + self.log.debug("Executing [PATCH] {}".format(entrypoint)) + url = "{}/{}".format(self._rest_url, entrypoint) + return self._do_rest_request( + RequestTypes.patch, + url, + **kwargs + ) + + def raw_get(self, entrypoint, **kwargs): + entrypoint = entrypoint.lstrip("/").rstrip("/") + self.log.debug("Executing [GET] {}".format(entrypoint)) + url = "{}/{}".format(self._rest_url, entrypoint) + return self._do_rest_request( + RequestTypes.get, + url, + **kwargs + ) + + def raw_delete(self, entrypoint, **kwargs): + entrypoint = entrypoint.lstrip("/").rstrip("/") + self.log.debug("Executing [DELETE] {}".format(entrypoint)) + url = "{}/{}".format(self._rest_url, entrypoint) + return self._do_rest_request( + RequestTypes.delete, + url, + **kwargs + ) + + def post(self, entrypoint, **kwargs): + return self.raw_post(entrypoint, json=kwargs) + + def put(self, entrypoint, **kwargs): + return self.raw_put(entrypoint, json=kwargs) + + def patch(self, entrypoint, **kwargs): + return self.raw_patch(entrypoint, json=kwargs) + + def get(self, entrypoint, **kwargs): + return self.raw_get(entrypoint, params=kwargs) + + def delete(self, entrypoint, **kwargs): + return self.raw_delete(entrypoint, params=kwargs) + + def get_event(self, event_id): + """Query full event data by id. + + Events received using event server do not contain full information. To + get the full event information is required to receive it explicitly. + + Args: + event_id (str): Id of event. + + Returns: + Dict[str, Any]: Full event data. + """ + + response = self.get("events/{}".format(event_id)) + response.raise_for_status() + return response.data + + def get_events( + self, + topics=None, + project_names=None, + states=None, + users=None, + include_logs=None, + fields=None + ): + """Get events from server with filtering options. + + Notes: + Not all event happen on a project. + + Args: + topics (Iterable[str]): Name of topics. + project_names (Iterable[str]): Project on which event happened. + states (Iterable[str]): Filtering by states. + users (Iterable[str]): Filtering by users who created/triggered + an event. + include_logs (bool): Query also log events. + fields (Union[Iterable[str], None]): Fields that should be received + for each event. + + Returns: + Generator[Dict[str, Any]]: Available events matching filters. + """ + + filters = {} + if topics is not None: + topics = set(topics) + if not topics: + return + filters["eventTopics"] = list(topics) + + if project_names is not None: + project_names = set(project_names) + if not project_names: + return + filters["projectName"] = list(project_names) + + if states is not None: + states = set(states) + if not states: + return + filters["eventStates"] = list(states) + + if users is not None: + users = set(users) + if not users: + return + filters["eventUsers"] = list(users) + + if include_logs is None: + include_logs = False + filters["includeLogsFilter"] = include_logs + + if not fields: + fields = DEFAULT_EVENT_FIELDS + + query = events_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 event in parsed_data["events"]: + yield event + + def update_event( + self, + event_id, + sender=None, + project_name=None, + status=None, + description=None, + summary=None, + payload=None + ): + kwargs = {} + for key, value in ( + ("sender", sender), + ("projectName", project_name), + ("status", status), + ("description", description), + ("summary", summary), + ("payload", payload), + ): + if value is not None: + kwargs[key] = value + response = self.patch( + "events/{}".format(event_id), + **kwargs + ) + response.raise_for_status() + + def dispatch_event( + self, + topic, + sender=None, + event_hash=None, + project_name=None, + username=None, + dependencies=None, + description=None, + summary=None, + payload=None, + finished=True, + store=True, + ): + """Dispatch event to server. + + Arg: + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + username (Optional[str]): Username which triggered event. + dependencies (Optional[list[str]]): List of event id deprendencies. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can be used + for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (bool): Mark event as finished on dispatch. + store (bool): Store event in event queue for possible future processing + otherwise is event send only to active listeners. + """ + + if summary is None: + summary = {} + if payload is None: + payload = {} + event_data = { + "topic": topic, + "sender": sender, + "hash": event_hash, + "project": project_name, + "user": username, + "dependencies": dependencies, + "description": description, + "summary": summary, + "payload": payload, + "finished": finished, + "store": store, + } + if self.post("events", **event_data): + self.log.debug("Dispatched event {}".format(topic)) + return True + self.log.error("Unable to dispatch event {}".format(topic)) + return False + + def enroll_event_job( + self, + source_topic, + target_topic, + sender, + description=None, + sequential=None + ): + """Enroll job based on events. + + Enroll will find first unprocessed event with 'source_topic' and will + create new event with 'target_topic' for it and return the new event + data. + + Use 'sequential' to control that only single target event is created + at same time. Creation of new target events is blocked while there is + at least one unfinished event with target topic, when set to 'True'. + This helps when order of events matter and more than one process using + the same target is running at the same time. + - Make sure the new event has updated status to '"finished"' status + when you're done with logic + + Target topic should not clash with other processes/services. + + Created target event have 'dependsOn' key where is id of source topic. + + Use-case: + - Service 1 is creating events with topic 'my.leech' + - Service 2 process 'my.leech' and uses target topic 'my.process' + - this service can run on 1..n machines + - all events must be processed in a sequence by their creation + time and only one event can be processed at a time + - in this case 'sequential' should be set to 'True' so only + one machine is actually processing events, but if one goes + down there are other that can take place + - Service 3 process 'my.leech' and uses target topic 'my.discover' + - this service can run on 1..n machines + - order of events is not important + - 'sequential' should be 'False' + + Args: + source_topic (str): Source topic to enroll. + target_topic (str): Topic of dependent event. + sender (str): Identifier of sender (e.g. service name or username). + description (str): Human readable text shown in target event. + sequential (bool): The source topic must be processed in sequence. + + Returns: + Union[None, dict[str, Any]]: None if there is no event matching + filters. Created event with 'target_topic'. + """ + + kwargs = { + "sourceTopic": source_topic, + "targetTopic": target_topic, + "sender": sender, + } + if sequential is not None: + kwargs["sequential"] = sequential + if description is not None: + kwargs["description"] = description + response = self.post("enroll", **kwargs) + if response.status_code == 204: + return None + elif response.status_code >= 400: + self.log.error(response.text) + return None + + return response.data + + def _download_file(self, url, filepath, chunk_size, progress): + dst_directory = os.path.dirname(filepath) + if not os.path.exists(dst_directory): + os.makedirs(dst_directory) + + kwargs = {"stream": True} + if self._session is None: + kwargs["headers"] = self.get_headers() + get_func = self._base_functions_mapping[RequestTypes.get] + else: + get_func = self._session_functions_mapping[RequestTypes.get] + + with open(filepath, "wb") as f_stream: + with get_func(url, **kwargs) as response: + response.raise_for_status() + progress.set_content_size(response.headers["Content-length"]) + for chunk in response.iter_content(chunk_size=chunk_size): + f_stream.write(chunk) + progress.add_transferred_chunk(len(chunk)) + + def download_file(self, endpoint, filepath, chunk_size=None, progress=None): + """Download file from AYON server. + + Endpoint can be full url (must start with 'base_url' of api object). + + Progress object can be used to track download. Can be used when + download happens in thread and other thread want to catch changes over + time. + + Args: + endpoint (str): Endpoint or URL to file that should be downloaded. + filepath (str): Path where file will be downloaded. + chunk_size (int): Size of chunks that are received in single loop. + progress (TransferProgress): Object that gives ability to track + download progress. + """ + + if not chunk_size: + # 1 MB chunk by default + chunk_size = 1024 * 1024 + + if endpoint.startswith(self._base_url): + url = endpoint + else: + endpoint = endpoint.lstrip("/").rstrip("/") + url = "{}/{}".format(self._rest_url, endpoint) + + # Create dummy object so the function does not have to check + # 'progress' variable everywhere + if progress is None: + progress = TransferProgress() + + progress.set_source_url(url) + progress.set_destination_url(filepath) + progress.set_started() + try: + self._download_file(url, filepath, chunk_size, progress) + + except Exception as exc: + progress.set_failed(str(exc)) + raise + + finally: + progress.set_transfer_done() + return progress + + def _upload_file(self, url, filepath, progress): + kwargs = {} + if self._session is None: + kwargs["headers"] = self.get_headers() + post_func = self._base_functions_mapping[RequestTypes.post] + else: + post_func = self._session_functions_mapping[RequestTypes.post] + + with open(filepath, "rb") as stream: + stream.seek(0, io.SEEK_END) + size = stream.tell() + stream.seek(0) + progress.set_content_size(size) + response = post_func(url, data=stream, **kwargs) + response.raise_for_status() + progress.set_transferred_size(size) + + def upload_file(self, endpoint, filepath, progress=None): + """Upload file to server. + + Todos: + Uploading with more detailed progress. + + Args: + endpoint (str): Endpoint or url where file will be uploaded. + filepath (str): Source filepath. + progress (TransferProgress): Object that gives ability to track + upload progress. + """ + + if endpoint.startswith(self._base_url): + url = endpoint + else: + endpoint = endpoint.lstrip("/").rstrip("/") + url = "{}/{}".format(self._rest_url, endpoint) + + # Create dummy object so the function does not have to check + # 'progress' variable everywhere + if progress is None: + progress = TransferProgress() + + progress.set_source_url(filepath) + progress.set_destination_url(url) + progress.set_started() + + try: + self._upload_file(url, filepath, progress) + + except Exception as exc: + progress.set_failed(str(exc)) + raise + + finally: + progress.set_transfer_done() + + def trigger_server_restart(self): + """Trigger server restart. + + Restart may be required when a change of specific value happened on + server. + """ + + result = self.post("system/restart") + if result.status_code != 204: + # TODO add better exception + raise ValueError("Failed to restart server") + + def query_graphql(self, query, variables=None): + """Execute GraphQl query. + + Args: + query (str): GraphQl query string. + variables (Union[None, dict[str, Any]): Variables that can be + used in query. + + Returns: + GraphQlResponse: Response from server. + """ + + data = {"query": query, "variables": variables or {}} + response = self._do_rest_request( + RequestTypes.post, + self._graphl_url, + json=data + ) + response.raise_for_status() + return GraphQlResponse(response) + + def get_graphql_schema(self): + return self.query_graphql(INTROSPECTION_QUERY).data + + def get_server_schema(self): + """Get server schema with info, url paths, components etc. + + Todos: + Cache schema - How to find out it is outdated? + + Returns: + Dict[str, Any]: Full server schema. + """ + + url = "{}/openapi.json".format(self._base_url) + response = self._do_rest_request(RequestTypes.get, url) + if response: + return response.data + return None + + def get_schemas(self): + """Get components schema. + + Name of components does not match entity type names e.g. 'project' is + under 'ProjectModel'. We should find out some mapping. Also there + are properties which don't have information about reference to object + e.g. 'config' has just object definition without reference schema. + + Returns: + Dict[str, Any]: Component schemas. + """ + + server_schema = self.get_server_schema() + return server_schema["components"]["schemas"] + + def get_attributes_schema(self, use_cache=True): + if not use_cache: + self.reset_attributes_schema() + + if self._attributes_schema is None: + result = self.get("attributes") + if result.status_code != 200: + raise UnauthorizedError( + "User must be authorized to receive attributes" + ) + self._attributes_schema = result.data + return copy.deepcopy(self._attributes_schema) + + def reset_attributes_schema(self): + self._attributes_schema = None + self._entity_type_attributes_cache = {} + + def set_attribute_config( + self, attribute_name, data, scope, position=None, builtin=False + ): + if position is None: + attributes = self.get("attributes").data["attributes"] + origin_attr = next( + ( + attr for attr in attributes + if attr["name"] == attribute_name + ), + None + ) + if origin_attr: + position = origin_attr["position"] + else: + position = len(attributes) + + response = self.put( + "attributes/{}".format(attribute_name), + data=data, + scope=scope, + position=position, + builtin=builtin + ) + if response.status_code != 204: + # TODO raise different exception + raise ValueError( + "Attribute \"{}\" was not created/updated. {}".format( + attribute_name, response.detail + ) + ) + + self.reset_attributes_schema() + + def remove_attribute_config(self, attribute_name): + """Remove attribute from server. + + This can't be un-done, please use carefully. + + Args: + attribute_name (str): Name of attribute to remove. + """ + + response = self.delete("attributes/{}".format(attribute_name)) + if response.status_code != 204: + # TODO raise different exception + raise ValueError( + "Attribute \"{}\" was not created/updated. {}".format( + attribute_name, response.detail + ) + ) + + self.reset_attributes_schema() + + def get_attributes_for_type(self, entity_type): + """Get attribute schemas available for an entity type. + + ``` + # Example attribute schema + { + # Common + "type": "integer", + "title": "Clip Out", + "description": null, + "example": 1, + "default": 1, + # These can be filled based on value of 'type' + "gt": null, + "ge": null, + "lt": null, + "le": null, + "minLength": null, + "maxLength": null, + "minItems": null, + "maxItems": null, + "regex": null, + "enum": null + } + ``` + + Args: + entity_type (str): Entity type for which should be attributes + received. + + Returns: + Dict[str, Dict[str, Any]]: Attribute schemas that are available + for entered entity type. + """ + attributes = self._entity_type_attributes_cache.get(entity_type) + if attributes is None: + attributes_schema = self.get_attributes_schema() + attributes = {} + for attr in attributes_schema["attributes"]: + if entity_type not in attr["scope"]: + continue + attr_name = attr["name"] + attributes[attr_name] = attr["data"] + + self._entity_type_attributes_cache[entity_type] = attributes + + return copy.deepcopy(attributes) + + def get_default_fields_for_type(self, entity_type): + """Default fields for entity type. + + Returns most of commonly used fields from server. + + Args: + entity_type (str): Name of entity type. + + Returns: + set[str]: Fields that should be queried from server. + """ + + attributes = self.get_attributes_for_type(entity_type) + if entity_type == "project": + return DEFAULT_PROJECT_FIELDS | { + "attrib.{}".format(attr) + for attr in attributes + } + + if entity_type == "folder": + return DEFAULT_FOLDER_FIELDS | { + "attrib.{}".format(attr) + for attr in attributes + } + + if entity_type == "task": + return DEFAULT_TASK_FIELDS | { + "attrib.{}".format(attr) + for attr in attributes + } + + if entity_type == "subset": + return DEFAULT_SUBSET_FIELDS | { + "attrib.{}".format(attr) + for attr in attributes + } + + if entity_type == "version": + return DEFAULT_VERSION_FIELDS | { + "attrib.{}".format(attr) + for attr in attributes + } + + if entity_type == "representation": + return ( + DEFAULT_REPRESENTATION_FIELDS + | REPRESENTATION_FILES_FIELDS + | { + "attrib.{}".format(attr) + for attr in attributes + } + ) + + raise ValueError("Unknown entity type \"{}\"".format(entity_type)) + + def get_addons_info(self, details=True): + """Get information about addons available on server. + + Args: + details (bool): Detailed data with information how to get client + code. + """ + + endpoint = "addons" + if details: + endpoint += "?details=1" + response = self.get(endpoint) + response.raise_for_status() + return response.data + + def download_addon_private_file( + self, + addon_name, + addon_version, + filename, + destination_dir, + destination_filename=None, + chunk_size=None, + progress=None, + ): + """Download a file from addon private files. + + This method requires to have authorized token available. Private files + are not under '/api' restpoint. + + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + filename (str): Filename in private folder on server. + destination_dir (str): Where the file should be downloaded. + destination_filename (str): Name of destination filename. Source + filename is used if not passed. + chunk_size (int): Download chunk size. + progress (TransferProgress): Object that gives ability to track + download progress. + + Returns: + str: Filepath to downloaded file. + """ + + if not destination_filename: + destination_filename = filename + dst_filepath = os.path.join(destination_dir, destination_filename) + # Filename can contain "subfolders" + dst_dirpath = os.path.dirname(dst_filepath) + if not os.path.exists(dst_dirpath): + os.makedirs(dst_dirpath) + + url = "{}/addons/{}/{}/private/{}".format( + self._base_url, + addon_name, + addon_version, + filename + ) + self.download_file( + url, dst_filepath, chunk_size=chunk_size, progress=progress + ) + return dst_filepath + + def get_dependencies_info(self): + """Information about dependency packages on server. + + Example data structure: + { + "packages": [ + { + "name": str, + "platform": str, + "checksum": str, + "sources": list[dict[str, Any]], + "supportedAddons": dict[str, str], + "pythonModules": dict[str, str] + } + ], + "productionPackage": str + } + + Returns: + dict[str, Any]: Information about dependency packages known for + server. + """ + + result = self.get("dependencies") + return result.data + + def update_dependency_info( + self, + name, + platform_name, + size, + checksum, + checksum_algorithm=None, + supported_addons=None, + python_modules=None, + sources=None + ): + """Update or create dependency package infor by it's identifiers. + + The endpoint can be used to create or update dependency package. + + Args: + name (str): Name of dependency package. + platform_name (Literal["windows", "linux", "darwin"]): Platform for + which is dependency package targeted. + size (int): Size of dependency package in bytes. + checksum (str): Checksum of archive file where dependecies are. + checksum_algorithm (str): Algorithm used to calculate checksum. + By default, is used 'md5' (defined by server). + supported_addons (Dict[str, str]): Name of addons for which was the + package created ('{"": "", ...}'). + python_modules (Dict[str, str]): Python modules in dependencies + package ('{"": "", ...}'). + sources (List[Dict[str, Any]]): Information about sources where + dependency package is available. + """ + + kwargs = { + key: value + for key, value in ( + ("checksumAlgorithm", checksum_algorithm), + ("supportedAddons", supported_addons), + ("pythonModules", python_modules), + ("sources", sources), + ) + if value + } + + response = self.put( + "dependencies", + name=name, + platform=platform_name, + size=size, + checksum=checksum, + **kwargs + ) + if response.status not in (200, 201): + raise ServerError("Failed to create/update dependency") + return response.data + + def download_dependency_package( + self, + package_name, + dst_directory, + filename, + platform_name=None, + chunk_size=None, + progress=None, + ): + """Download dependency package from server. + + This method requires to have authorized token available. The package is + only downloaded. + + Args: + package_name (str): Name of package to download. + dst_directory (str): Where the file should be downloaded. + filename (str): Name of destination filename. + platform_name (str): Name of platform for which the dependency + package is targetter. Default value is current platform. + chunk_size (int): Download chunk size. + progress (TransferProgress): Object that gives ability to track + download progress. + + Returns: + str: Filepath to downloaded file. + """ + if platform_name is None: + platform_name = platform.system().lower() + + package_filepath = os.path.join(dst_directory, filename) + self.download_file( + "dependencies/{}/{}".format(package_name, platform_name), + package_filepath, + chunk_size=chunk_size, + progress=progress + ) + return package_filepath + + def upload_dependency_package( + self, filepath, package_name, platform_name=None, progress=None + ): + """Upload dependency package to server. + + Args: + filepath (str): Path to a package file. + package_name (str): Name of package. Must be unique. + platform_name (str): For which platform is the package targeted. + progress (Optional[TransferProgress]): Object to keep track about + upload state. + """ + + if platform_name is None: + platform_name = platform.system().lower() + + self.upload_file( + "dependencies/{}/{}".format(package_name, platform_name), + filepath, + progress=progress + ) + + def delete_dependency_package(self, package_name, platform_name=None): + """Remove dependency package for specific platform. + + Args: + package_name (str): Name of package to remove. + platform_name (Optional[str]): Which platform of the package should + be removed. Current platform is used if not passed. + """ + + if platform_name is None: + platform_name = platform.system().lower() + + response = self.delete( + "dependencies/{}/{}".format(package_name, platform_name), + ) + if response.status != 200: + raise ServerError("Failed to delete dependency file") + return response.data + + # Anatomy presets + def get_project_anatomy_presets(self): + """Anatomy presets available on server. + + Content has basic information about presets. Example output: + [ + { + "name": "netflix_VFX", + "primary": false, + "version": "1.0.0" + }, + { + ... + }, + ... + ] + + Returns: + list[dict[str, str]]: Anatomy presets available on server. + """ + + result = self.get("anatomy/presets") + result.raise_for_status() + return result.data.get("presets") or [] + + def get_project_anatomy_preset(self, preset_name=None): + """Anatomy preset values by name. + + Get anatomy preset values by preset name. Primary preset is returned + if preset name is set to 'None'. + + Args: + Union[str, None]: Preset name. + + Returns: + dict[str, Any]: Anatomy preset values. + """ + + if preset_name is None: + preset_name = "_" + result = self.get("anatomy/presets/{}".format(preset_name)) + result.raise_for_status() + return result.data + + def get_project_roots_by_site(self, project_name): + """Root overrides per site name. + + Method is based on logged user and can't be received for any other + user on server. + + Output will contain only roots per site id used by logged user. + + Args: + project_name (str): Name of project. + + Returns: + dict[str, dict[str, str]]: Root values by root name by site id. + """ + + result = self.get("projects/{}/roots".format(project_name)) + result.raise_for_status() + return result.data + + def get_project_roots_for_site(self, project_name, site_id=None): + """Root overrides for site. + + If site id is not passed a site set in current api object is used + instead. + + Args: + project_name (str): Name of project. + site_id (Optional[str]): Id of site for which want to receive + site overrides. + + Returns: + dict[str, str]: Root values by root name or None if + site does not have overrides. + """ + + if site_id is None: + site_id = self.site_id + + if site_id is None: + return {} + roots = self.get_project_roots_by_site(project_name) + return roots.get(site_id, {}) + + def get_addon_settings_schema( + self, addon_name, addon_version, project_name=None + ): + """Sudio/Project settings schema of an addon. + + Project schema may look differently as some enums are based on project + values. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (Union[str, None]): Schema for specific project or + default studio schemas. + + Returns: + dict[str, Any]: Schema of studio/project settings. + """ + + endpoint = "addons/{}/{}/schema".format(addon_name, addon_version) + if project_name: + endpoint += "/{}".format(project_name) + result = self.get(endpoint) + result.raise_for_status() + return result.data + + def get_addon_site_settings_schema(self, addon_name, addon_version): + """Site settings schema of an addon. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + + Returns: + dict[str, Any]: Schema of site settings. + """ + + result = self.get("addons/{}/{}/siteSettings/schema".format( + addon_name, addon_version + )) + result.raise_for_status() + return result.data + + def get_addon_studio_settings( + self, + addon_name, + addon_version, + variant=None + ): + """Addon studio settings. + + Receive studio settings for specific version of an addon. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (str): Name of settings variant. By default, is used + 'default_settings_variant' passed on init. + + Returns: + dict[str, Any]: Addon settings. + """ + + if variant is None: + variant = self.default_settings_variant + + query_items = {} + if variant: + query_items["variant"] = variant + query = prepare_query_string(query_items) + + result = self.get( + "addons/{}/{}/settings{}".format(addon_name, addon_version, query) + ) + result.raise_for_status() + return result.data + + def get_addon_project_settings( + self, + addon_name, + addon_version, + project_name, + variant=None, + site_id=None, + use_site=True + ): + """Addon project settings. + + Receive project settings for specific version of an addon. The settings + may be with site overrides when enabled. + + Site id is filled with current connection site id if not passed. To + make sure any site id is used set 'use_site' to 'False'. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Name of project for which the settings are + received. + variant (str): Name of settings variant. By default, is used + 'production'. + site_id (str): Name of site which is used for site overrides. Is + filled with connection 'site_id' attribute if not passed. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + + Returns: + dict[str, Any]: Addon settings. + """ + + if not use_site: + site_id = None + elif not site_id: + site_id = self.site_id + + query_items = {} + if site_id: + query_items["site"] = site_id + + if variant is None: + variant = self.default_settings_variant + + if variant: + query_items["variant"] = variant + + query = prepare_query_string(query_items) + result = self.get( + "addons/{}/{}/settings/{}{}".format( + addon_name, addon_version, project_name, query + ) + ) + result.raise_for_status() + return result.data + + def get_addon_settings( + self, + addon_name, + addon_version, + project_name=None, + variant=None, + site_id=None, + use_site=True + ): + """Receive addon settings. + + Receive addon settings based on project name value. Some arguments may + be ignored if 'project_name' is set to 'None'. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Name of project for which the settings are + received. A studio settings values are received if is 'None'. + variant (str): Name of settings variant. By default, is used + 'production'. + site_id (str): Name of site which is used for site overrides. Is + filled with connection 'site_id' attribute if not passed. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + + Returns: + dict[str, Any]: Addon settings. + """ + + if project_name is None: + return self.get_addon_studio_settings( + addon_name, addon_version, variant + ) + return self.get_addon_project_settings( + addon_name, addon_version, project_name, variant, site_id, use_site + ) + + def get_addon_site_settings( + self, addon_name, addon_version, site_id=None + ): + """Site settings of an addon. + + If site id is not available an empty dictionary is returned. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + site_id (str): Name of site for which should be settings returned. + using 'site_id' attribute if not passed. + + Returns: + dict[str, Any]: Site settings. + """ + + if site_id is None: + site_id = self.site_id + + if not site_id: + return {} + + query = prepare_query_string({"site": site_id}) + result = self.get("addons/{}/{}/siteSettings{}".format( + addon_name, addon_version, query + )) + result.raise_for_status() + return result.data + + def get_addons_studio_settings(self, variant=None, only_values=True): + """All addons settings in one bulk. + + Args: + variant (Literal[production, staging]): Variant of settings. By + default, is used 'production'. + only_values (Optional[bool]): Output will contain only settings + values without metadata about addons. + + Returns: + dict[str, Any]: Settings of all addons on server. + """ + + query_values = {} + if variant: + query_values["variant"] = variant + query = prepare_query_string(query_values) + response = self.get("settings/addons{}".format(query)) + response.raise_for_status() + output = response.data + if only_values: + output = output["settings"] + return output + + def get_addons_project_settings( + self, + project_name, + variant=None, + site_id=None, + use_site=True, + only_values=True + ): + """Project settings of all addons. + + Server returns information about used addon versions, so full output + looks like: + { + "settings": {...}, + "addons": {...} + } + + The output can be limited to only values. To do so is 'only_values' + argument which is by default set to 'True'. In that case output + contains only value of 'settings' key. + + Args: + project_name (str): Name of project for which are settings + received. + variant (Optional[Literal[production, staging]]): Variant of + settings. By default, is used 'production'. + site_id (Optional[str]): Id of site for which want to receive + site overrides. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + only_values (Optional[bool]): Output will contain only settings + values without metadata about addons. + + Returns: + dict[str, Any]: Settings of all addons on server for passed + project. + """ + + query_values = { + "project": project_name + } + if variant: + query_values["variant"] = variant + + if use_site: + if not site_id: + site_id = self.default_settings_variant + if site_id: + query_values["site"] = site_id + query = prepare_query_string(query_values) + response = self.get("settings/addons{}".format(query)) + response.raise_for_status() + output = response.data + if only_values: + output = output["settings"] + return output + + def get_addons_settings( + self, + project_name=None, + variant=None, + site_id=None, + use_site=True, + only_values=True + ): + """Universal function to receive all addon settings. + + Based on 'project_name' will receive studio settings or project + settings. In case project is not passed is 'site_id' ignored. + + Args: + project_name (Optional[str]): Name of project for which should be + settings received. + variant (Optional[Literal[production, staging]]): Settings variant. + By default, is used 'production'. + site_id (Optional[str]): Id of site for which want to receive + site overrides. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + only_values (Optional[bool]): Only settings values will be + returned. By default, is set to 'True'. + """ + + if project_name is None: + return self.get_addons_studio_settings(variant, only_values) + + return self.get_addons_project_settings( + project_name, variant, site_id, use_site, only_values + ) + + # Entity getters + def get_rest_project(self, project_name): + """Query project by name. + + This call returns project with anatomy data. + + Args: + project_name (str): Name of project. + + Returns: + Union[Dict[str, Any], None]: Project entity data or 'None' if + project was not found. + """ + + if not project_name: + return None + + response = self.get("projects/{}".format(project_name)) + if response.status == 200: + return response.data + return None + + def get_rest_projects(self, active=True, library=None): + """Query available project entities. + + User must be logged in. + + Args: + active (Union[bool, None]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (bool): Filter standard/library projects. Both are + returned if 'None' is passed. + + Returns: + Generator[Dict[str, Any]]: Available projects. + """ + + for project_name in self.get_project_names(active, library): + project = self.get_rest_project(project_name) + if project: + yield project + + def get_rest_entity_by_id(self, project_name, entity_type, entity_id): + """Get entity using REST on a project by its id. + + Args: + project_name (str): Name of project where entity is. + entity_type (Literal["folder", "task", "subset", "version"]): The + entity type which should be received. + entity_id (str): Id of entity. + + Returns: + dict[str, Any]: Received entity data. + """ + + if not all((project_name, entity_type, entity_id)): + return None + + entity_endpoint = "{}s".format(entity_type) + response = self.get("projects/{}/{}/{}".format( + project_name, entity_endpoint, entity_id + )) + if response.status == 200: + return response.data + return None + + def get_rest_folder(self, project_name, folder_id): + return self.get_rest_entity_by_id(project_name, "folder", folder_id) + + def get_rest_task(self, project_name, task_id): + return self.get_rest_entity_by_id(project_name, "task", task_id) + + def get_rest_subset(self, project_name, subset_id): + return self.get_rest_entity_by_id(project_name, "subset", subset_id) + + def get_rest_version(self, project_name, version_id): + return self.get_rest_entity_by_id(project_name, "version", version_id) + + def get_rest_representation(self, project_name, representation_id): + return self.get_rest_entity_by_id( + project_name, "representation", representation_id + ) + + def get_project_names(self, active=True, library=None): + """Receive available project names. + + User must be logged in. + + Args: + active (Union[bool, None[): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (bool): Filter standard/library projects. Both are + returned if 'None' is passed. + + Returns: + List[str]: List of available project names. + """ + + query_keys = {} + if active is not None: + query_keys["active"] = "true" if active else "false" + + if library is not None: + query_keys["library"] = "true" if active else "false" + query = "" + if query_keys: + query = "?{}".format(",".join([ + "{}={}".format(key, value) + for key, value in query_keys.items() + ])) + + response = self.get("projects{}".format(query), **query_keys) + response.raise_for_status() + data = response.data + project_names = [] + if data: + for project in data["projects"]: + project_names.append(project["name"]) + return project_names + + def get_projects( + self, active=True, library=None, fields=None, own_attributes=False + ): + """Get projects. + + Args: + active (Union[bool, None]): Filter active or inactive projects. + Filter is disabled when 'None' is passed. + library (Union[bool, None]): Filter library projects. Filter is + disabled when 'None' is passed. + fields (Union[Iterable[str], None]): fields to be queried + for project. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Generator[Dict[str, Any]]: Queried projects. + """ + + if fields is None: + use_rest = True + else: + use_rest = False + fields = set(fields) + if own_attributes: + fields.add("ownAttrib") + for field in fields: + if field.startswith("config"): + use_rest = True + break + + if use_rest: + for project in self.get_rest_projects(active, library): + if own_attributes: + fill_own_attribs(project) + yield project + + else: + query = projects_graphql_query(fields) + for parsed_data in query.continuous_query(self): + for project in parsed_data["projects"]: + if own_attributes: + fill_own_attribs(project) + yield project + + def get_project(self, project_name, fields=None, own_attributes=False): + """Get project. + + Args: + project_name (str): Name of project. + fields (Union[Iterable[str], None]): fields to be queried + for project. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[Dict[str, Any], None]: Project entity data or None + if project was not found. + """ + + use_rest = True + if fields is not None: + use_rest = False + _fields = set() + for field in fields: + if field.startswith("config") or field == "data": + use_rest = True + break + _fields.add(field) + + fields = _fields + + if use_rest: + project = self.get_rest_project(project_name) + if own_attributes: + fill_own_attribs(project) + return project + + if own_attributes: + field.add("ownAttrib") + query = project_graphql_query(fields) + query.set_variable_value("projectName", project_name) + + parsed_data = query.query(self) + + project = parsed_data["project"] + if project is not None: + project["name"] = project_name + if own_attributes: + fill_own_attribs(project) + return project + + def get_folders( + self, + project_name, + folder_ids=None, + folder_paths=None, + folder_names=None, + parent_ids=None, + active=True, + fields=None, + own_attributes=False + ): + """Query folders from server. + + Todos: + Folder name won't be unique identifier so we should add folder path + filtering. + + Notes: + Filter 'active' don't have direct filter in GraphQl. + + Args: + project_name (str): Name of project. + folder_ids (Iterable[str]): Folder ids to filter. + folder_paths (Iterable[str]): Folder paths used for filtering. + folder_names (Iterable[str]): Folder names used for filtering. + parent_ids (Iterable[str]): Ids of folder parents. Use 'None' + if folder is direct child of project. + active (Union[bool, None]): Filter active/inactive folders. + Both are returned if is set to None. + fields (Union[Iterable[str], None]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Generator[dict[str, Any]]: Queried folder entities. + """ + + if not project_name: + return + + filters = { + "projectName": project_name + } + if folder_ids is not None: + folder_ids = set(folder_ids) + if not folder_ids: + return + filters["folderIds"] = list(folder_ids) + + if folder_paths is not None: + folder_paths = set(folder_paths) + if not folder_paths: + return + filters["folderPaths"] = list(folder_paths) + + if folder_names is not None: + folder_names = set(folder_names) + if not folder_names: + return + filters["folderNames"] = list(folder_names) + + if parent_ids is not None: + parent_ids = set(parent_ids) + if not parent_ids: + return + if None in parent_ids: + # Replace 'None' with '"root"' which is used during GraphQl + # query for parent ids filter for folders without folder + # parent + parent_ids.remove(None) + parent_ids.add("root") + + if project_name in parent_ids: + # Replace project name with '"root"' which is used during + # GraphQl query for parent ids filter for folders without + # folder parent + parent_ids.remove(project_name) + parent_ids.add("root") + + filters["parentFolderIds"] = list(parent_ids) + + if fields: + fields = set(fields) + else: + fields = self.get_default_fields_for_type("folder") + + use_rest = False + if "data" in fields: + use_rest = True + fields = {"id"} + + if active is not None: + fields.add("active") + + if own_attributes and not use_rest: + fields.add("ownAttrib") + + query = folders_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for folder in parsed_data["project"]["folders"]: + if active is not None and active is not folder["active"]: + continue + + if use_rest: + folder = self.get_rest_folder(project_name, folder["id"]) + + if own_attributes: + fill_own_attribs(folder) + yield folder + + def get_tasks( + self, + project_name, + task_ids=None, + task_names=None, + task_types=None, + folder_ids=None, + active=True, + fields=None, + own_attributes=False + ): + """Query task entities from server. + + Args: + project_name (str): Name of project. + task_ids (Iterable[str]): Task ids to filter. + task_names (Iterable[str]): Task names used for filtering. + task_types (Itartable[str]): Task types used for filtering. + folder_ids (Iterable[str]): Ids of task parents. Use 'None' + if folder is direct child of project. + active (Union[bool, None]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Union[Iterable[str], None]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Generator[dict[str, Any]]: Queried task entities. + """ + + if not project_name: + return + + filters = { + "projectName": project_name + } + + if task_ids is not None: + task_ids = set(task_ids) + if not task_ids: + return + filters["taskIds"] = list(task_ids) + + if task_names is not None: + task_names = set(task_names) + if not task_names: + return + filters["taskNames"] = list(task_names) + + if task_types is not None: + task_types = set(task_types) + if not task_types: + return + filters["taskTypes"] = list(task_types) + + if folder_ids is not None: + folder_ids = set(folder_ids) + if not folder_ids: + return + filters["folderIds"] = list(folder_ids) + + if not fields: + fields = self.get_default_fields_for_type("task") + + fields = set(fields) + + use_rest = False + if "data" in fields: + use_rest = True + fields = {"id"} + + if active is not None: + fields.add("active") + + if own_attributes: + fields.add("ownAttrib") + + query = tasks_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for task in parsed_data["project"]["tasks"]: + if active is not None and active is not task["active"]: + continue + + if use_rest: + task = self.get_rest_task(project_name, task["id"]) + + if own_attributes: + fill_own_attribs(task) + yield task + + def get_task_by_name( + self, + project_name, + folder_id, + task_name, + fields=None, + own_attributes=False + ): + """Query task entity by name and folder id. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + task_name (str): Task name + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Task entity data or None if was not found. + """ + + for task in self.get_tasks( + project_name, + folder_ids=[folder_id], + task_names=[task_name], + active=None, + fields=fields, + own_attributes=own_attributes + ): + return task + return None + + def get_task_by_id( + self, + project_name, + task_id, + fields=None, + own_attributes=False + ): + """Query task entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + task_id (str): Task id. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Task entity data or None if was not found. + """ + + for task in self.get_tasks( + project_name, + task_ids=[task_id], + active=None, + fields=fields, + own_attributes=own_attributes + ): + return task + return None + + + def get_folder_by_id( + self, + project_name, + folder_id, + fields=None, + own_attributes=False + ): + """Query folder entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Folder entity data or None if was not found. + """ + + folders = self.get_folders( + project_name, + folder_ids=[folder_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_by_path( + self, + project_name, + folder_path, + fields=None, + own_attributes=False + ): + """Query folder entity by path. + + Folder path is a path to folder with all parent names joined by slash. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_path (str): Folder path. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Folder entity data or None if was not found. + """ + + folders = self.get_folders( + project_name, + folder_paths=[folder_path], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_by_name( + self, + project_name, + folder_name, + fields=None, + own_attributes=False + ): + """Query folder entity by path. + + Warnings: + Folder name is not a unique identifier of a folder. Function is + kept for OpenPype 3 compatibility. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_name (str): Folder name. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Folder entity data or None if was not found. + """ + + folders = self.get_folders( + project_name, + folder_names=[folder_name], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_ids_with_subsets(self, project_name, folder_ids=None): + """Find folders which have at least one subset. + + Folders that have at least one subset should be immutable, so they + should not change path -> change of name or name of any parent + is not possible. + + Args: + project_name (str): Name of project. + folder_ids (Union[Iterable[str], None]): Limit folder ids filtering + to a set of folders. If set to None all folders on project are + checked. + + Returns: + set[str]: Folder ids that have at least one subset. + """ + + if folder_ids is not None: + folder_ids = set(folder_ids) + if not folder_ids: + return set() + + query = folders_graphql_query({"id"}) + query.set_variable_value("projectName", project_name) + query.set_variable_value("folderHasSubsets", True) + if folder_ids: + query.set_variable_value("folderIds", list(folder_ids)) + + parsed_data = query.query(self) + folders = parsed_data["project"]["folders"] + return { + folder["id"] + for folder in folders + } + + def _filter_subset( + self, project_name, subset, active, own_attributes, use_rest + ): + if active is not None and subset["active"] is not active: + return None + + if use_rest: + subset = self.get_rest_subset(project_name, subset["id"]) + + if own_attributes: + fill_own_attribs(subset) + + return subset + + def get_subsets( + self, + project_name, + subset_ids=None, + subset_names=None, + folder_ids=None, + names_by_folder_ids=None, + active=True, + fields=None, + own_attributes=False + ): + """Query subsets from server. + + Todos: + Separate 'name_by_folder_ids' filtering to separated method. It + cannot be combined with some other filters. + + Args: + project_name (str): Name of project. + subset_ids (Iterable[str]): Task ids to filter. + subset_names (Iterable[str]): Task names used for filtering. + folder_ids (Iterable[str]): Ids of task parents. Use 'None' + if folder is direct child of project. + names_by_folder_ids (dict[str, Iterable[str]]): Subset name + filtering by folder id. + active (Union[bool, None]): Filter active/inactive subsets. + Both are returned if is set to None. + fields (Union[Iterable[str], None]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Generator[dict[str, Any]]: Queried subset entities. + """ + + if not project_name: + return + + if subset_ids is not None: + subset_ids = set(subset_ids) + if not subset_ids: + return + + filter_subset_names = None + if subset_names is not None: + filter_subset_names = set(subset_names) + if not filter_subset_names: + return + + filter_folder_ids = None + if folder_ids is not None: + filter_folder_ids = set(folder_ids) + if not filter_folder_ids: + return + + # This will disable 'folder_ids' and 'subset_names' filters + # - maybe could be enhanced in future? + if names_by_folder_ids is not None: + filter_subset_names = set() + filter_folder_ids = set() + + for folder_id, names in names_by_folder_ids.items(): + if folder_id and names: + filter_folder_ids.add(folder_id) + filter_subset_names |= set(names) + + if not filter_subset_names or not filter_folder_ids: + return + + # Convert fields and add minimum required fields + if fields: + fields = set(fields) | {"id"} + else: + fields = self.get_default_fields_for_type("subset") + + use_rest = False + if "data" in fields: + use_rest = True + fields = {"id"} + + if active is not None: + fields.add("active") + + if own_attributes: + fields.add("ownAttrib") + + # Add 'name' and 'folderId' if 'names_by_folder_ids' filter is entered + if names_by_folder_ids: + fields.add("name") + fields.add("folderId") + + # Prepare filters for query + filters = { + "projectName": project_name + } + if filter_folder_ids: + filters["folderIds"] = list(filter_folder_ids) + + if subset_ids: + filters["subsetIds"] = list(subset_ids) + + if filter_subset_names: + filters["subsetNames"] = list(filter_subset_names) + + query = subsets_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + parsed_data = query.query(self) + + subsets = parsed_data.get("project", {}).get("subsets", []) + # Filter subsets by 'names_by_folder_ids' + if names_by_folder_ids: + subsets_by_folder_id = collections.defaultdict(list) + for subset in subsets: + filtered_subset = self._filter_subset( + project_name, subset, active, own_attributes, use_rest + ) + if filtered_subset is not None: + folder_id = filtered_subset["folderId"] + subsets_by_folder_id[folder_id].append(filtered_subset) + + for folder_id, names in names_by_folder_ids.items(): + for folder_subset in subsets_by_folder_id[folder_id]: + if folder_subset["name"] in names: + yield folder_subset + + else: + for subset in subsets: + filtered_subset = self._filter_subset( + project_name, subset, active, own_attributes, use_rest + ) + if filtered_subset is not None: + yield filtered_subset + + + def get_subset_by_id( + self, + project_name, + subset_id, + fields=None, + own_attributes=False + ): + """Query subset entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + subset_id (str): Subset id. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Subset entity data or None if was not found. + """ + + subsets = self.get_subsets( + project_name, + subset_ids=[subset_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for subset in subsets: + return subset + return None + + def get_subset_by_name( + self, + project_name, + subset_name, + folder_id, + fields=None, + own_attributes=False + ): + """Query subset entity by name and folder id. + + Args: + project_name (str): Name of project where to look for queried + entities. + subset_name (str): Subset name. + folder_id (str): Folder id (Folder is a parent of subsets). + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Subset entity data or None if was not found. + """ + + subsets = self.get_subsets( + project_name, + subset_names=[subset_name], + folder_ids=[folder_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for subset in subsets: + return subset + return None + + def get_subset_families(self, project_name, subset_ids=None): + """Families of subsets from a project. + + Args: + project_name (str): Name of project where to look for queried + entities. + subset_ids (Union[None, Iterable[str]]): Limit filtering to set + of subset ids. + + Returns: + set[str]: Families found on subsets. + """ + + if subset_ids is not None: + subsets = self.get_subsets( + project_name, + subset_ids=subset_ids, + fields=["family"], + active=None, + ) + return { + subset["family"] + for subset in subsets + } + + query = GraphQlQuery("SubsetFamilies") + project_name_var = query.add_variable( + "projectName", "String!", project_name + ) + project_query = query.add_field("project") + project_query.set_filter("name", project_name_var) + project_query.add_field("subsetFamilies") + + parsed_data = query.query(self) + + return set(parsed_data.get("project", {}).get("subsetFamilies", [])) + + def get_versions( + self, + project_name, + version_ids=None, + subset_ids=None, + versions=None, + hero=True, + standard=True, + latest=None, + active=True, + fields=None, + own_attributes=False + ): + """Get version entities based on passed filters from server. + + Args: + project_name (str): Name of project where to look for versions. + version_ids (Iterable[str]): Version ids used for version + filtering. + subset_ids (Iterable[str]): Subset ids used for version filtering. + versions (Iterable[int]): Versions we're interested in. + hero (bool): Receive also hero versions when set to true. + standard (bool): Receive versions which are not hero when + set to true. + latest (bool): Return only latest version of standard versions. + This can be combined only with 'standard' attribute + set to True. + active (Union[bool, None]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Union[Iterable[str], None]): Fields to be queried + for version. All possible folder fields are returned + if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Generator[Dict[str, Any]]: Queried version entities. + """ + + if not fields: + fields = self.get_default_fields_for_type("version") + fields = set(fields) + + if active is not None: + fields.add("active") + + # Make sure fields have minimum required fields + fields |= {"id", "version"} + + use_rest = False + if "data" in fields: + use_rest = True + fields = {"id"} + + if own_attributes: + fields.add("ownAttrib") + + filters = { + "projectName": project_name + } + if version_ids is not None: + version_ids = set(version_ids) + if not version_ids: + return + filters["versionIds"] = list(version_ids) + + if subset_ids is not None: + subset_ids = set(subset_ids) + if not subset_ids: + return + filters["subsetIds"] = list(subset_ids) + + # TODO versions can't be used as fitler at this moment! + if versions is not None: + versions = set(versions) + if not versions: + return + filters["versions"] = list(versions) + + if not hero and not standard: + return + + queries = [] + # Add filters based on 'hero' and 'standard' + # NOTE: There is not a filter to "ignore" hero versions or to get + # latest and hero version + # - if latest and hero versions should be returned it must be done in + # 2 graphql queries + if standard and not latest: + # This query all versions standard + hero + # - hero must be filtered out if is not enabled during loop + query = versions_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + queries.append(query) + else: + if hero: + # Add hero query if hero is enabled + hero_query = versions_graphql_query(fields) + for attr, filter_value in filters.items(): + hero_query.set_variable_value(attr, filter_value) + + hero_query.set_variable_value("heroOnly", True) + queries.append(hero_query) + + if standard: + standard_query = versions_graphql_query(fields) + for attr, filter_value in filters.items(): + standard_query.set_variable_value(attr, filter_value) + + if latest: + standard_query.set_variable_value("latestOnly", True) + queries.append(standard_query) + + for query in queries: + for parsed_data in query.continuous_query(self): + for version in parsed_data["project"]["versions"]: + if active is not None and version["active"] is not active: + continue + + if not hero and version["version"] < 0: + continue + + if use_rest: + version = self.get_rest_version( + project_name, version["id"] + ) + + if own_attributes: + fill_own_attribs(version) + + yield version + + def get_version_by_id( + self, + project_name, + version_id, + fields=None, + own_attributes=False + ): + """Query version entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version_id (str): Version id. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Version entity data or None if was not found. + """ + + versions = self.get_versions( + project_name, + version_ids=[version_id], + active=None, + hero=True, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_version_by_name( + self, + project_name, + version, + subset_id, + fields=None, + own_attributes=False + ): + """Query version entity by version and subset id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version (int): Version of version entity. + subset_id (str): Subset id. Subset is a parent of version. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Version entity data or None if was not found. + """ + + versions = self.get_versions( + project_name, + subset_ids=[subset_id], + versions=[version], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_hero_version_by_id( + self, + project_name, + version_id, + fields=None, + own_attributes=False + ): + """Query hero version entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version_id (int): Hero version id. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Version entity data or None if was not found. + """ + + versions = self.get_hero_versions( + project_name, + version_ids=[version_id], + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_hero_version_by_subset_id( + self, + project_name, + subset_id, + fields=None, + own_attributes=False + ): + """Query hero version entity by subset id. + + Only one hero version is available on a subset. + + Args: + project_name (str): Name of project where to look for queried + entities. + subset_id (int): Subset id. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Version entity data or None if was not found. + """ + + versions = self.get_hero_versions( + project_name, + subset_ids=[subset_id], + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_hero_versions( + self, + project_name, + subset_ids=None, + version_ids=None, + active=True, + fields=None, + own_attributes=False + ): + """Query hero versions by multiple filters. + + Only one hero version is available on a subset. + + Args: + project_name (str): Name of project where to look for queried + entities. + subset_ids (int): Subset ids. + version_ids (int): Version ids. + active (Union[bool, None]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Version entity data or None if was not found. + """ + + return self.get_versions( + project_name, + version_ids=version_ids, + subset_ids=subset_ids, + hero=True, + standard=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + + def get_last_versions( + self, + project_name, + subset_ids, + active=True, + fields=None, + own_attributes=False + ): + """Query last version entities by subset ids. + + Args: + project_name (str): Project where to look for representation. + subset_ids (Iterable[str]): Subset ids. + active (Union[bool, None]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Union[Iterable[str], None]): fields to be queried + for representations. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + dict[str, dict[str, Any]]: Last versions by subset id. + """ + + versions = self.get_versions( + project_name, + subset_ids=subset_ids, + latest=True, + active=active, + fields=fields, + own_attributes=own_attributes + ) + return { + version["parent"]: version + for version in versions + } + + def get_last_version_by_subset_id( + self, + project_name, + subset_id, + active=True, + fields=None, + own_attributes=False + ): + """Query last version entity by subset id. + + Args: + project_name (str): Project where to look for representation. + subset_id (str): Subset id. + active (Union[bool, None]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Union[Iterable[str], None]): fields to be queried + for representations. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict[str, Any], None]: Queried version entity or None. + """ + + versions = self.get_versions( + project_name, + subset_ids=[subset_id], + latest=True, + active=active, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_last_version_by_subset_name( + self, + project_name, + subset_name, + folder_id, + active=True, + fields=None, + own_attributes=False + ): + """Query last version entity by subset name and folder id. + + Args: + project_name (str): Project where to look for representation. + subset_name (str): Subset name. + folder_id (str): Folder id. + active (Union[bool, None]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Union[Iterable[str], None]): fields to be queried + for representations. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict[str, Any], None]: Queried version entity or None. + """ + + if not folder_id: + return None + + subset = self.get_subset_by_name( + project_name, subset_name, folder_id, fields=["_id"] + ) + if not subset: + return None + return self.get_last_version_by_subset_id( + project_name, + subset["id"], + active=active, + fields=fields, + own_attributes=own_attributes + ) + + def version_is_latest(self, project_name, version_id): + """Is version latest from a subset. + + Args: + project_name (str): Project where to look for representation. + version_id (str): Version id. + + Returns: + bool: Version is latest or not. + """ + + query = GraphQlQuery("VersionIsLatest") + project_name_var = query.add_variable( + "projectName", "String!", project_name + ) + version_id_var = query.add_variable( + "versionId", "String!", version_id + ) + project_query = query.add_field("project") + project_query.set_filter("name", project_name_var) + version_query = project_query.add_field("version") + version_query.set_filter("id", version_id_var) + subset_query = version_query.add_field("subset") + latest_version_query = subset_query.add_field("latestVersion") + latest_version_query.add_field("id") + + parsed_data = query.query(self) + latest_version = ( + parsed_data["project"]["version"]["subset"]["latestVersion"] + ) + return latest_version["id"] == version_id + + def get_representations( + self, + project_name, + representation_ids=None, + representation_names=None, + version_ids=None, + names_by_version_ids=None, + active=True, + fields=None, + own_attributes=False + ): + """Get representation entities based on passed filters from server. + + Todos: + Add separated function for 'names_by_version_ids' filtering. + Because can't be combined with others. + + Args: + project_name (str): Name of project where to look for versions. + representation_ids (Iterable[str]): Representation ids used for + representation filtering. + representation_names (Iterable[str]): Representation names used for + representation filtering. + version_ids (Iterable[str]): Version ids used for + representation filtering. Versions are parents of + representations. + names_by_version_ids (bool): Find representations by names and + version ids. This filter discard all other filters. + active (Union[bool, None]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Union[Iterable[str], None]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Generator[Dict[str, Any]]: Queried representation entities. + """ + + if not fields: + fields = self.get_default_fields_for_type("representation") + fields = set(fields) + + use_rest = False + if "data" in fields: + use_rest = True + fields = {"id"} + + if active is not None: + fields.add("active") + + if own_attributes: + fields.add("ownAttrib") + + filters = { + "projectName": project_name + } + + if representation_ids is not None: + representation_ids = set(representation_ids) + if not representation_ids: + return + filters["representationIds"] = list(representation_ids) + + version_ids_filter = None + representaion_names_filter = None + if names_by_version_ids is not None: + version_ids_filter = set() + representaion_names_filter = set() + for version_id, names in names_by_version_ids.items(): + version_ids_filter.add(version_id) + representaion_names_filter |= set(names) + + if not version_ids_filter or not representaion_names_filter: + return + + else: + if representation_names is not None: + representaion_names_filter = set(representation_names) + if not representaion_names_filter: + return + + if version_ids is not None: + version_ids_filter = set(version_ids) + if not version_ids_filter: + return + + if version_ids_filter: + filters["versionIds"] = list(version_ids_filter) + + if representaion_names_filter: + filters["representationNames"] = list(representaion_names_filter) + + query = representations_graphql_query(fields) + + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for repre in parsed_data["project"]["representations"]: + if active is not None and active is not repre["active"]: + continue + + if use_rest: + repre = self.get_rest_representation( + project_name, repre["id"] + ) + + if "context" in repre: + orig_context = repre["context"] + context = {} + if orig_context and orig_context != "null": + context = json.loads(orig_context) + repre["context"] = context + + if own_attributes: + fill_own_attribs(repre) + yield repre + + def get_representation_by_id( + self, + project_name, + representation_id, + fields=None, + own_attributes=False + ): + """Query representation entity from server based on id filter. + + Args: + project_name (str): Project where to look for representation. + representation_id (str): Id of representation. + fields (Union[Iterable[str], None]): fields to be queried + for representations. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict[str, Any], None]: Queried representation entity or None. + """ + + representations = self.get_representations( + project_name, + representation_ids=[representation_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for representation in representations: + return representation + return None + + def get_representation_by_name( + self, + project_name, + representation_name, + version_id, + fields=None, + own_attributes=False + ): + """Query representation entity by name and version id. + + Args: + project_name (str): Project where to look for representation. + representation_name (str): Representation name. + version_id (str): Version id. + fields (Union[Iterable[str], None]): fields to be queried + for representations. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict[str, Any], None]: Queried representation entity or None. + """ + + representations = self.get_representations( + project_name, + representation_names=[representation_name], + version_ids=[version_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for representation in representations: + return representation + return None + + def get_representations_parents(self, project_name, representation_ids): + """Find representations parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + + Returns: + dict[str, RepresentationParents]: Parent entities by + representation id. + """ + + if not representation_ids: + return {} + + project = self.get_project(project_name) + repre_ids = set(representation_ids) + output = { + repre_id: RepresentationParents(None, None, None, None) + for repre_id in representation_ids + } + + version_fields = self.get_default_fields_for_type("version") + subset_fields = self.get_default_fields_for_type("subset") + folder_fields = self.get_default_fields_for_type("folder") + + query = representations_parents_qraphql_query( + version_fields, subset_fields, folder_fields + ) + query.set_variable_value("projectName", project_name) + query.set_variable_value("representationIds", list(repre_ids)) + + parsed_data = query.query(self) + for repre in parsed_data["project"]["representations"]: + repre_id = repre["id"] + version = repre.pop("version") + subset = version.pop("subset") + folder = subset.pop("folder") + output[repre_id] = RepresentationParents( + version, subset, folder, project + ) + + return output + + def get_representation_parents(self, project_name, representation_id): + """Find representation parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + + Returns: + RepresentationParents: Representation parent entities. + """ + + if not representation_id: + return None + + parents_by_repre_id = self.get_representations_parents( + project_name, [representation_id] + ) + return parents_by_repre_id[representation_id] + + def get_repre_ids_by_context_filters( + self, + project_name, + context_filters, + representation_names=None, + version_ids=None + ): + """Find representation ids which match passed context filters. + + Each representation has context integrated on representation entity in + database. The context may contain project, folder, task name or subset, + family and many more. This implementation gives option to quickly + filter representation based on representation data in database. + + Context filters have defined structure. To define filter of nested + subfield use dot '.' as delimiter (For example 'task.name'). + Filter values can be regex filters. String or 're.Pattern' can be used. + + Args: + project_name (str): Project where to look for representations. + context_filters (dict[str, list[str]]): Filters of context fields. + representation_names (Iterable[str]): Representation names, can be + used as additional filter for representations by their names. + version_ids (Iterable[str]): Version ids, can be used as additional + filter for representations by their parent ids. + + Returns: + list[str]: Representation ids that match passed filters. + + Example: + The function returns just representation ids so if entities are + required for funtionality they must be queried afterwards by + their ids. + >>> project_name = "testProject" + >>> filters = { + ... "task.name": ["[aA]nimation"], + ... "subset": [".*[Mm]ain"] + ... } + >>> repre_ids = get_repre_ids_by_context_filters( + ... project_name, filters) + >>> repres = get_representations(project_name, repre_ids) + """ + + if not isinstance(context_filters, dict): + raise TypeError( + "Expected 'dict' got {}".format(str(type(context_filters))) + ) + + filter_body = {} + if representation_names is not None: + if not representation_names: + return [] + filter_body["names"] = list(set(representation_names)) + + if version_ids is not None: + if not version_ids: + return [] + filter_body["versionIds"] = list(set(version_ids)) + + body_context_filters = [] + for key, filters in context_filters.items(): + if not isinstance(filters, (set, list, tuple)): + raise TypeError( + "Expected 'set', 'list', 'tuple' got {}".format( + str(type(filters)))) + + + new_filters = set() + for filter_value in filters: + if isinstance(filter_value, PatternType): + filter_value = filter_value.pattern + new_filters.add(filter_value) + + body_context_filters.append({ + "key": key, + "values": list(new_filters) + }) + + response = self.post( + "projects/{}/repreContextFilter".format(project_name), + context=body_context_filters, + **filter_body + ) + response.raise_for_status() + return response.data["ids"] + + def get_workfiles_info( + self, + project_name, + workfile_ids=None, + task_ids=None, + paths=None, + fields=None, + own_attributes=False + ): + """Workfile info entities by passed filters. + + Args: + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + fields (Union[Iterable[str], None]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Generator[dict[str, Any]]: Queried workfile info entites. + """ + + filters = {"projectName": project_name} + if task_ids is not None: + task_ids = set(task_ids) + if not task_ids: + return + filters["taskIds"] = list(task_ids) + + if paths is not None: + paths = set(paths) + if not paths: + return + filters["paths"] = list(paths) + + if workfile_ids is not None: + workfile_ids = set(workfile_ids) + if not workfile_ids: + return + filters["workfileIds"] = list(workfile_ids) + + if not fields: + fields = DEFAULT_WORKFILE_INFO_FIELDS + fields = set(fields) + if own_attributes: + fields.add("ownAttrib") + + query = workfiles_info_graphql_query(fields) + + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for workfile_info in parsed_data["project"]["workfiles"]: + if own_attributes: + fill_own_attribs(workfile_info) + yield workfile_info + + def get_workfile_info( + self, project_name, task_id, path, fields=None, own_attributes=False + ): + """Workfile info entity by task id and workfile path. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Union[Iterable[str], None]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict[str, Any], None]: Workfile info entity or None. + """ + + if not task_id or not path: + return None + + for workfile_info in self.get_workfiles_info( + project_name, + task_ids=[task_id], + paths=[path], + fields=fields, + own_attributes=own_attributes + ): + return workfile_info + return None + + def get_workfile_info_by_id( + self, project_name, workfile_id, fields=None, own_attributes=False + ): + """Workfile info entity by id. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Union[Iterable[str], None]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict[str, Any], None]: Workfile info entity or None. + """ + + if not workfile_id: + return None + + for workfile_info in self.get_workfiles_info( + project_name, + workfile_ids=[workfile_id], + fields=fields, + own_attributes=own_attributes + ): + return workfile_info + return None + + 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. + + 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. + entity_type (str): Entity type which passed entity id represents. + entity_id (str): Entity id for which thumbnail should be returned. + thumbnail_id (str): Prepared thumbnail id from entity. Used only + to check if thumbnail was already cached. + + Returns: + Union[str, None]: Path to downlaoded thumbnail or none if entity + does not have any (or if user does not have permissions). + """ + + # 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 entity_type in ( + "folder", + "version", + "workfile", + ): + entity_type += "s" + + # Receive thumbnail content from server + result = 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 it's path + return self._thumbnail_cache.store_thumbnail( + project_name, + thumbnail_id, + result.content, + result.content_type + ) + + def get_folder_thumbnail( + self, project_name, folder_id, thumbnail_id=None + ): + """Prepared method to receive thumbnail for folder entity. + + Args: + project_name (str): Project under which the entity is located. + folder_id (str): Folder id for which thumbnail should be returned. + thumbnail_id (str): Prepared thumbnail id from entity. Used only + to check if thumbnail was already cached. + + Returns: + Union[str, None]: Path to downlaoded thumbnail or none if entity + does not have any (or if user does not have permissions). + """ + + return self.get_thumbnail( + project_name, "folder", folder_id, thumbnail_id + ) + + def get_version_thumbnail( + self, project_name, version_id, thumbnail_id=None + ): + """Prepared method to receive thumbnail for version entity. + + Args: + project_name (str): Project under which the entity is located. + version_id (str): Version id for which thumbnail should be + returned. + thumbnail_id (str): Prepared thumbnail id from entity. Used only + to check if thumbnail was already cached. + + Returns: + Union[str, None]: Path to downlaoded thumbnail or none if entity + does not have any (or if user does not have permissions). + """ + + return self.get_thumbnail( + project_name, "version", version_id, thumbnail_id + ) + + def get_workfile_thumbnail( + self, project_name, workfile_id, thumbnail_id=None + ): + """Prepared method to receive thumbnail for workfile entity. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Worfile id for which thumbnail should be + returned. + thumbnail_id (str): Prepared thumbnail id from entity. Used only + to check if thumbnail was already cached. + + Returns: + Union[str, None]: Path to downlaoded thumbnail or none if entity + does not have any (or if user does not have permissions). + """ + + return self.get_thumbnail( + project_name, "workfile", workfile_id, thumbnail_id + ) + + def create_project( + self, + project_name, + project_code, + library_project=False, + preset_name=None + ): + """Create project using Ayon settings. + + This project creation function is not validating project entity on + creation. It is because project entity is created blindly with only + minimum required information about project which is it's name, code. + + Entered project name must be unique and project must not exist yet. + + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. + + Args: + project_name (str): New project name. Should be unique. + project_code (str): Project's code should be unique too. + library_project (bool): Project is library project. + preset_name (str): Name of anatomy preset. Default is used if not + passed. + con (ServerAPI): Connection to server with logged user. + + Raises: + ValueError: When project name already exists. + + Returns: + Dict[str, Any]: Created project entity. + """ + + if self.get_project(project_name): + raise ValueError("Project with name \"{}\" already exists".format( + project_name + )) + + if not PROJECT_NAME_REGEX.match(project_name): + raise ValueError(( + "Project name \"{}\" contain invalid characters" + ).format(project_name)) + + preset = self.get_project_anatomy_preset(preset_name) + + result = self.post( + "projects", + name=project_name, + code=project_code, + anatomy=preset, + library=library_project + ) + + if result.status != 201: + details = "Unknown details ({})".format(result.status) + if result.data: + details = result.data.get("detail") or details + raise ValueError("Failed to create project \"{}\": {}".format( + project_name, details + )) + + return self.get_project(project_name) + + def delete_project(self, project_name): + """Delete project from server. + + This will completely remove project from server without any step back. + + Args: + project_name (str): Project name that will be removed. + """ + + if not self.get_project(project_name): + raise ValueError("Project with name \"{}\" was not found".format( + project_name + )) + + result = self.delete("projects/{}".format(project_name)) + if result.status_code != 204: + raise ValueError( + "Failed to delete project \"{}\". {}".format( + project_name, result.data["detail"] + ) + ) + + def create_thumbnail(self, project_name, src_filepath): + """Create new thumbnail on server from passed path. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + Returns: + str: Created thumbnail id. + + Todos: + Define more specific exceptions for thumbnail creation. + + Raises: + ValueError: When thumbnail creation fails (due to many reasons). + """ + + if not os.path.exists(src_filepath): + raise ValueError("Entered filepath does not exist.") + + ext = os.path.splitext(src_filepath)[-1].lower() + if ext == ".png": + mime_type = "image/png" + + elif ext in (".jpeg", ".jpg"): + mime_type = "image/jpeg" + + else: + raise ValueError( + "Thumbnail source file has unknown extensions {}".format(ext)) + + with open(src_filepath, "rb") as stream: + content = stream.read() + + response = self.raw_post( + "projects/{}/thumbnails".format(project_name), + headers={"Content-Type": mime_type}, + data=content + ) + if response.status_code != 200: + _detail = response.data.get("detail") + details = "" + if _detail: + details = " {}".format(_detail) + raise ValueError( + "Failed to create thumbnail.{}".format(details)) + return response.data["id"] + + def send_batch_operations( + self, + project_name, + operations, + can_fail=False, + raise_on_fail=True + ): + """Post multiple CRUD operations to server. + + When multiple changes should be made on server side this is the best + way to go. It is possible to pass multiple operations to process on a + server side and do the changes in a transaction. + + Args: + project_name (str): On which project should be operations + processed. + operations (list[dict[str, Any]]): Operations to be processed. + can_fail (bool): Server will try to process all operations even if + one of them fails. + raise_on_fail (bool): Raise exception if an operation fails. + You can handle failed operations on your own when set to + 'False'. + + Raises: + ValueError: Operations can't be converted to json string. + FailedOperations: When output does not contain server operations + or 'raise_on_fail' is enabled and any operation fails. + + Returns: + list[dict[str, Any]]: Operations result with process details. + """ + + if not operations: + return [] + + body_by_id = {} + operations_body = [] + for operation in operations: + if not operation: + continue + + op_id = operation.get("id") + if not op_id: + op_id = create_entity_id() + operation["id"] = op_id + + try: + body = json.loads( + json.dumps(operation, default=entity_data_json_default) + ) + except: + raise ValueError("Couldn't json parse body: {}".format( + json.dumps( + operation, indent=4, default=failed_json_default + ) + )) + + body_by_id[op_id] = body + operations_body.append(body) + + if not operations_body: + return [] + + result = self.post( + "projects/{}/operations".format(project_name), + operations=operations_body, + canFail=can_fail + ) + + op_results = result.get("operations") + if op_results is None: + raise FailedOperations( + "Operation failed. Content: {}".format(str(result)) + ) + + if result.get("success") or not raise_on_fail: + return op_results + + for op_result in op_results: + if not op_result["success"]: + operation_id = op_result["id"] + raise FailedOperations(( + "Operation \"{}\" failed with data:\n{}\nDetail: {}." + ).format( + operation_id, + json.dumps(body_by_id[operation_id], indent=4), + op_result["detail"], + )) + return op_results diff --git a/openpype/vendor/python/ayon/ayon_api/thumbnails.py b/openpype/vendor/python/ayon/ayon_api/thumbnails.py new file mode 100644 index 0000000000..50acd94dcb --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/thumbnails.py @@ -0,0 +1,219 @@ +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 diff --git a/openpype/vendor/python/ayon/ayon_api/utils.py b/openpype/vendor/python/ayon/ayon_api/utils.py new file mode 100644 index 0000000000..28971f7de5 --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/utils.py @@ -0,0 +1,451 @@ +import re +import datetime +import uuid +import string +import collections +try: + # Python 3 + from urllib.parse import urlparse, urlencode +except ImportError: + # Python 2 + from urlparse import urlparse + from urllib import urlencode + +import requests +import unidecode + +from .exceptions import UrlError + +REMOVED_VALUE = object() +SLUGIFY_WHITELIST = string.ascii_letters + string.digits +SLUGIFY_SEP_WHITELIST = " ,./\\;:!|*^#@~+-_=" + +RepresentationParents = collections.namedtuple( + "RepresentationParents", + ("version", "subset", "folder", "project") +) + + +def prepare_query_string(key_values): + """Prepare data to query string. + + If there are any values a query starting with '?' is returned otherwise + an empty string. + + Args: + dict[str, Any]: Query values. + + Returns: + str: Query string. + """ + + if not key_values: + return "" + return "?{}".format(urlencode(key_values)) + + +def create_entity_id(): + return uuid.uuid1().hex + + +def convert_entity_id(entity_id): + if not entity_id: + return None + + if isinstance(entity_id, uuid.UUID): + return entity_id.hex + + try: + return uuid.UUID(entity_id).hex + + except (TypeError, ValueError, AttributeError): + pass + return None + + +def convert_or_create_entity_id(entity_id=None): + output = convert_entity_id(entity_id) + if output is None: + output = create_entity_id() + return output + + +def entity_data_json_default(value): + if isinstance(value, datetime.datetime): + return int(value.timestamp()) + + raise TypeError( + "Object of type {} is not JSON serializable".format(str(type(value))) + ) + + +def slugify_string( + input_string, + separator="_", + slug_whitelist=SLUGIFY_WHITELIST, + split_chars=SLUGIFY_SEP_WHITELIST, + min_length=1, + lower=False, + make_set=False, +): + """Slugify a text string. + + This function removes transliterates input string to ASCII, removes + special characters and use join resulting elements using + specified separator. + + Args: + input_string (str): Input string to slugify + separator (str): A string used to separate returned elements + (default: "_") + slug_whitelist (str): Characters allowed in the output + (default: ascii letters, digits and the separator) + split_chars (str): Set of characters used for word splitting + (there is a sane default) + lower (bool): Convert to lower-case (default: False) + make_set (bool): Return "set" object instead of string. + min_length (int): Minimal length of an element (word). + + Returns: + Union[str, Set[str]]: Based on 'make_set' value returns slugified + string. + """ + + tmp_string = unidecode.unidecode(input_string) + if lower: + tmp_string = tmp_string.lower() + + parts = [ + # Remove all characters that are not in whitelist + re.sub("[^{}]".format(re.escape(slug_whitelist)), "", part) + # Split text into part by split characters + for part in re.split("[{}]".format(re.escape(split_chars)), tmp_string) + ] + # Filter text parts by length + filtered_parts = [ + part + for part in parts + if len(part) >= min_length + ] + if make_set: + return set(filtered_parts) + return separator.join(filtered_parts) + + +def failed_json_default(value): + return "< Failed value {} > {}".format(type(value), str(value)) + + +def prepare_attribute_changes(old_entity, new_entity, replace=False): + attrib_changes = {} + new_attrib = new_entity.get("attrib") + old_attrib = old_entity.get("attrib") + if new_attrib is None: + if not replace: + return attrib_changes + new_attrib = {} + + if old_attrib is None: + return new_attrib + + for attr, new_attr_value in new_attrib.items(): + old_attr_value = old_attrib.get(attr) + if old_attr_value != new_attr_value: + attrib_changes[attr] = new_attr_value + + if replace: + for attr in old_attrib: + if attr not in new_attrib: + attrib_changes[attr] = REMOVED_VALUE + + return attrib_changes + + +def prepare_entity_changes(old_entity, new_entity, replace=False): + """Prepare changes of entities.""" + + changes = {} + for key, new_value in new_entity.items(): + if key == "attrib": + continue + + old_value = old_entity.get(key) + if old_value != new_value: + changes[key] = new_value + + if replace: + for key in old_entity: + if key not in new_entity: + changes[key] = REMOVED_VALUE + + attr_changes = prepare_attribute_changes(old_entity, new_entity, replace) + if attr_changes: + changes["attrib"] = attr_changes + return changes + + +def _try_parse_url(url): + try: + return urlparse(url) + except BaseException: + return None + + +def _try_connect_to_server(url): + try: + # TODO add validation if the url lead to Ayon server + # - thiw won't validate if the url lead to 'google.com' + requests.get(url) + + except BaseException: + return False + return True + + +def login_to_server(url, username, password): + """Use login to the server to receive token. + + Args: + url (str): Server url. + username (str): User's username. + password (str): User's password. + + Returns: + Union[str, None]: User's token if login was successfull. + Otherwise 'None'. + """ + + headers = {"Content-Type": "application/json"} + response = requests.post( + "{}/api/auth/login".format(url), + headers=headers, + json={ + "name": username, + "password": password + } + ) + token = None + # 200 - success + # 401 - invalid credentials + # * - other issues + if response.status_code == 200: + token = response.json()["token"] + return token + + +def logout_from_server(url, token): + """Logout from server and throw token away. + + Args: + url (str): Url from which should be logged out. + token (str): Token which should be used to log out. + """ + + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer {}".format(token) + } + requests.post( + url + "/api/auth/logout", + headers=headers + ) + + +def is_token_valid(url, token): + """Check if token is valid. + + Args: + url (str): Server url. + token (str): User's token. + + Returns: + bool: True if token is valid. + """ + + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer {}".format(token) + } + response = requests.get( + "{}/api/users/me".format(url), + headers=headers + ) + return response.status_code == 200 + + +def validate_url(url): + """Validate url if is valid and server is available. + + Validation checks if can be parsed as url and contains scheme. + + Function will try to autofix url thus will return modified url when + connection to server works. + + ```python + my_url = "my.server.url" + try: + # Store new url + validated_url = validate_url(my_url) + + except UrlError: + # Handle invalid url + ... + ``` + + Args: + url (str): Server url. + + Returns: + Url which was used to connect to server. + + Raises: + UrlError: Error with short description and hints for user. + """ + + stripperd_url = url.strip() + if not stripperd_url: + raise UrlError( + "Invalid url format. Url is empty.", + title="Invalid url format", + hints=["url seems to be empty"] + ) + + # Not sure if this is good idea? + modified_url = stripperd_url.rstrip("/") + parsed_url = _try_parse_url(modified_url) + universal_hints = [ + "does the url work in browser?" + ] + if parsed_url is None: + raise UrlError( + "Invalid url format. Url cannot be parsed as url \"{}\".".format( + modified_url + ), + title="Invalid url format", + hints=universal_hints + ) + + # Try add 'https://' scheme if is missing + # - this will trigger UrlError if both will crash + if not parsed_url.scheme: + new_url = "https://" + modified_url + if _try_connect_to_server(new_url): + return new_url + + if _try_connect_to_server(modified_url): + return modified_url + + hints = [] + if "/" in parsed_url.path or not parsed_url.scheme: + new_path = parsed_url.path.split("/")[0] + if not parsed_url.scheme: + new_path = "https://" + new_path + + hints.append( + "did you mean \"{}\"?".format(parsed_url.scheme + new_path) + ) + + raise UrlError( + "Couldn't connect to server on \"{}\"".format(url), + title="Couldn't connect to server", + hints=hints + universal_hints + ) + + +class TransferProgress: + """Object to store progress of download/upload from/to server.""" + + def __init__(self): + self._started = False + self._transfer_done = False + self._transfered = 0 + self._content_size = None + + self._failed = False + self._fail_reason = None + + self._source_url = "N/A" + self._destination_url = "N/A" + + def get_content_size(self): + return self._content_size + + def set_content_size(self, content_size): + if self._content_size is not None: + raise ValueError("Content size was set more then once") + self._content_size = content_size + + def get_started(self): + return self._started + + def set_started(self): + if self._started: + raise ValueError("Progress already started") + self._started = True + + def get_transfer_done(self): + return self._transfer_done + + def set_transfer_done(self): + if self._transfer_done: + raise ValueError("Progress was already marked as done") + if not self._started: + raise ValueError("Progress didn't start yet") + self._transfer_done = True + + def get_failed(self): + return self._failed + + def get_fail_reason(self): + return self._fail_reason + + def set_failed(self, reason): + self._fail_reason = reason + self._failed = True + + def get_transferred_size(self): + return self._transfered + + def set_transferred_size(self, transfered): + self._transfered = transfered + + def add_transferred_chunk(self, chunk_size): + self._transfered += chunk_size + + def get_source_url(self): + return self._source_url + + def set_source_url(self, url): + self._source_url = url + + def get_destination_url(self): + return self._destination_url + + def set_destination_url(self, url): + self._destination_url = url + + @property + def is_running(self): + if ( + not self.started + or self.done + or self.failed + ): + return False + return True + + @property + def transfer_progress(self): + if self._content_size is None: + return None + return (self._transfered * 100.0) / float(self._content_size) + + content_size = property(get_content_size, set_content_size) + started = property(get_started) + transfer_done = property(get_transfer_done) + failed = property(get_failed) + fail_reason = property(get_fail_reason) + source_url = property(get_source_url, set_source_url) + destination_url = property(get_destination_url, set_destination_url) + content_size = property(get_content_size, set_content_size) + transferred_size = property(get_transferred_size, set_transferred_size) diff --git a/openpype/vendor/python/ayon/ayon_api/version.py b/openpype/vendor/python/ayon/ayon_api/version.py new file mode 100644 index 0000000000..a65f885820 --- /dev/null +++ b/openpype/vendor/python/ayon/ayon_api/version.py @@ -0,0 +1,2 @@ +"""Package declaring Python API for Ayon server.""" +__version__ = "0.1.16" \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index f915832fb8..ee7003d565 100644 --- a/poetry.lock +++ b/poetry.lock @@ -302,24 +302,6 @@ files = [ pycodestyle = ">=2.10.0" tomli = {version = "*", markers = "python_version < \"3.11\""} -[[package]] -name = "ayon-python-api" -version = "0.1.16" -description = "AYON Python API" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "ayon-python-api-0.1.16.tar.gz", hash = "sha256:666110954dd75b2be1699a29b4732cfb0bcb09d01f64fba4449bfc8ac1fb43f1"}, - {file = "ayon_python_api-0.1.16-py3-none-any.whl", hash = "sha256:bbcd6df1f80ddf32e653a1bb31289cb5fd1a8bea36ab4c8e6aef08c41b6393de"}, -] - -[package.dependencies] -appdirs = ">=1,<2" -requests = ">=2.27.1" -six = ">=1.15" -Unidecode = ">=1.2.0" - [[package]] name = "babel" version = "2.11.0" diff --git a/pyproject.toml b/pyproject.toml index ebd7ea127d..2427c447c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,6 @@ requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" -ayon-python-api = "^0.1" opencolorio = "^2.2.0" Unidecode = "^1.2" From 66c4522f9c28c9b2760642f3b1ce7f9d798b0279 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 3 Apr 2023 11:07:40 +0200 Subject: [PATCH 207/446] update pyside6 to 6.4.3 (#4764) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2427c447c3..32982bed8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,7 +124,7 @@ version = "5.15.2" [openpype.qtbinding.darwin] package = "PySide6" -version = "6.4.1" +version = "6.4.3" [openpype.qtbinding.linux] package = "PySide2" From e42aebc1f277cef9dc3f195954a0ba817697127b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 Apr 2023 11:12:02 +0200 Subject: [PATCH 208/446] Global: custom location for OP local versions (#4673) * OP-5221 - updated settings for custom location of artist zip folder * OP-5221 - updated igniter for custom location of artist zip folder Introduced new function Updated data_dir only after access to Mongo (DB) * OP-5221 - pushed resolving of local folder to OpenPypeVersion Logic in OpenPypeVersion is used even in openpype_version.py * OP-5221 - updates after review * OP-5221 - fix paths should be single paths * OP-5221 - refactor to cls * OP-5221 - refactor Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * OP-5221 - fix defaults for single paths Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * OP-5221 - remove unwanted line Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * OP-5221 - update look of Settings Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- igniter/bootstrap_repos.py | 40 +++++++++++++++---- igniter/install_thread.py | 15 ++++++- igniter/tools.py | 20 ++++++++++ igniter/update_thread.py | 2 + .../defaults/system_settings/general.json | 5 +++ .../schemas/system_schema/schema_general.json | 18 ++++++++- openpype/settings/handlers.py | 1 + start.py | 7 +++- 8 files changed, 97 insertions(+), 11 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 6c7c834062..4cf00375bf 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -25,7 +25,8 @@ from .user_settings import ( from .tools import ( get_openpype_global_settings, get_openpype_path_from_settings, - get_expected_studio_version_str + get_expected_studio_version_str, + get_local_openpype_path_from_settings ) @@ -61,6 +62,8 @@ class OpenPypeVersion(semver.VersionInfo): """ path = None + + _local_openpype_path = None # this should match any string complying with https://semver.org/ _VERSION_REGEX = re.compile(r"(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P[a-zA-Z\d\-.]*))?(?:\+(?P[a-zA-Z\d\-.]*))?") # noqa: E501 _installed_version = None @@ -289,6 +292,23 @@ class OpenPypeVersion(semver.VersionInfo): """ return os.getenv("OPENPYPE_PATH") + @classmethod + def get_local_openpype_path(cls): + """Path to unzipped versions. + + By default it should be user appdata, but could be overridden by + settings. + """ + if cls._local_openpype_path: + return cls._local_openpype_path + + settings = get_openpype_global_settings(os.environ["OPENPYPE_MONGO"]) + data_dir = get_local_openpype_path_from_settings(settings) + if not data_dir: + data_dir = Path(user_data_dir("openpype", "pypeclub")) + cls._local_openpype_path = data_dir + return data_dir + @classmethod def openpype_path_is_set(cls): """Path to OpenPype zip directory is set.""" @@ -319,9 +339,8 @@ class OpenPypeVersion(semver.VersionInfo): list: of compatible versions available on the machine. """ - # DEPRECATED: backwards compatible way to look for versions in root - dir_to_search = Path(user_data_dir("openpype", "pypeclub")) - versions = OpenPypeVersion.get_versions_from_directory(dir_to_search) + dir_to_search = cls.get_local_openpype_path() + versions = cls.get_versions_from_directory(dir_to_search) return list(sorted(set(versions))) @@ -533,17 +552,15 @@ class BootstrapRepos: """ # vendor and app used to construct user data dir - self._vendor = "pypeclub" - self._app = "openpype" + self._message = message self._log = log.getLogger(str(__class__)) - self.data_dir = Path(user_data_dir(self._app, self._vendor)) + self.set_data_dir(None) self.secure_registry = OpenPypeSecureRegistry("mongodb") self.registry = OpenPypeSettingsRegistry() self.zip_filter = [".pyc", "__pycache__"] self.openpype_filter = [ "openpype", "schema", "LICENSE" ] - self._message = message # dummy progress reporter def empty_progress(x: int): @@ -554,6 +571,13 @@ class BootstrapRepos: progress_callback = empty_progress self._progress_callback = progress_callback + def set_data_dir(self, data_dir): + if not data_dir: + self.data_dir = Path(user_data_dir("openpype", "pypeclub")) + else: + self._print(f"overriding local folder: {data_dir}") + self.data_dir = data_dir + @staticmethod def get_version_path_from_list( version: str, version_list: list) -> Union[Path, None]: diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 4723e6adfb..1d55213de7 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -14,7 +14,11 @@ from .bootstrap_repos import ( OpenPypeVersion ) -from .tools import validate_mongo_connection +from .tools import ( + get_openpype_global_settings, + get_local_openpype_path_from_settings, + validate_mongo_connection +) class InstallThread(QtCore.QThread): @@ -80,6 +84,15 @@ class InstallThread(QtCore.QThread): return os.environ["OPENPYPE_MONGO"] = self._mongo + if not validate_mongo_connection(self._mongo): + self.message.emit(f"Cannot connect to {self._mongo}", True) + self._set_result(-1) + return + + global_settings = get_openpype_global_settings(self._mongo) + data_dir = get_local_openpype_path_from_settings(global_settings) + bs.set_data_dir(data_dir) + self.message.emit( f"Detecting installed OpenPype versions in {bs.data_dir}", False) diff --git a/igniter/tools.py b/igniter/tools.py index 79235b2329..af5cbe70a9 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -188,6 +188,26 @@ def get_openpype_path_from_settings(settings: dict) -> Union[str, None]: return next((path for path in paths if os.path.exists(path)), None) +def get_local_openpype_path_from_settings(settings: dict) -> Union[str, None]: + """Get OpenPype local path from global settings. + + Used to download and unzip OP versions. + Args: + settings (dict): settings from DB. + + Returns: + path to OpenPype or None if not found + """ + path = ( + settings + .get("local_openpype_path", {}) + .get(platform.system().lower()) + ) + if path: + return Path(path) + return None + + def get_expected_studio_version_str( staging=False, global_settings=None ) -> str: diff --git a/igniter/update_thread.py b/igniter/update_thread.py index e98c95f892..0223477d0a 100644 --- a/igniter/update_thread.py +++ b/igniter/update_thread.py @@ -48,6 +48,8 @@ class UpdateThread(QtCore.QThread): """ bs = BootstrapRepos( progress_callback=self.set_progress, message=self.message) + + bs.set_data_dir(OpenPypeVersion.get_local_openpype_path()) version_path = bs.install_version(self._openpype_version) self._set_result(version_path) diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json index d2994d1a62..496c37cd4d 100644 --- a/openpype/settings/defaults/system_settings/general.json +++ b/openpype/settings/defaults/system_settings/general.json @@ -15,6 +15,11 @@ "darwin": [], "linux": [] }, + "local_openpype_path": { + "windows": "", + "darwin": "", + "linux": "" + }, "production_version": "", "staging_version": "", "version_check_interval": 5 diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index d6c22fe54c..2609441061 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -128,8 +128,12 @@ { "type": "collapsible-wrap", "label": "OpenPype deployment control", - "collapsible": false, + "collapsible": true, "children": [ + { + "type": "label", + "label": "Define location accessible by artist machine to check for zip updates with Openpype code." + }, { "type": "path", "key": "openpype_path", @@ -138,6 +142,18 @@ "multipath": true, "require_restart": true }, + { + "type": "label", + "label": "Define custom location for artist machine where to unzip versions of Openpype code. By default it is in user app data folder." + }, + { + "type": "path", + "key": "local_openpype_path", + "label": "Custom Local Versions Folder", + "multiplatform": true, + "multipath": false, + "require_restart": true + }, { "type": "splitter" }, diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index ab7cdd058c..1d4c838f1a 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -189,6 +189,7 @@ class SettingsStateInfo: class SettingsHandler(object): global_keys = { "openpype_path", + "local_openpype_path", "admin_password", "log_to_server", "disk_mapping", diff --git a/start.py b/start.py index f8d65dc221..2160f03493 100644 --- a/start.py +++ b/start.py @@ -268,6 +268,7 @@ from igniter import BootstrapRepos # noqa: E402 from igniter.tools import ( get_openpype_global_settings, get_openpype_path_from_settings, + get_local_openpype_path_from_settings, validate_mongo_connection, OpenPypeVersionNotFound, OpenPypeVersionIncompatible @@ -1039,6 +1040,10 @@ def boot(): # find its versions there and bootstrap them. openpype_path = get_openpype_path_from_settings(global_settings) + # Check if local versions should be installed in custom folder and not in + # user app data + data_dir = get_local_openpype_path_from_settings(global_settings) + bootstrap.set_data_dir(data_dir) if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(OPENPYPE_ROOT)) else: @@ -1076,7 +1081,7 @@ def boot(): _print(f"!!! {e}", True) sys.exit(1) # validate version - _print(f">>> Validating version [ {str(version_path)} ]") + _print(f">>> Validating version in frozen [ {str(version_path)} ]") result = bootstrap.validate_openpype_version(version_path) if not result[0]: _print(f"!!! Invalid version: {result[1]}", True) From 4015d39c157a5287ca925baf50e84d12d0c66159 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 3 Apr 2023 11:14:26 +0200 Subject: [PATCH 209/446] AYON: General issues (#4763) * move ayon api to common python vendor * fix PySide6 support --- ayon_start.py | 13 ------------- common/ayon_common/connection/ui/login_window.py | 16 ++++++++++------ common/ayon_common/connection/ui/widgets.py | 2 +- .../python/{ayon => common}/ayon_api/__init__.py | 0 .../python/{ayon => common}/ayon_api/_api.py | 0 .../{ayon => common}/ayon_api/constants.py | 0 .../{ayon => common}/ayon_api/entity_hub.py | 0 .../python/{ayon => common}/ayon_api/events.py | 0 .../{ayon => common}/ayon_api/exceptions.py | 0 .../python/{ayon => common}/ayon_api/graphql.py | 0 .../{ayon => common}/ayon_api/graphql_queries.py | 0 .../{ayon => common}/ayon_api/operations.py | 0 .../{ayon => common}/ayon_api/server_api.py | 0 .../{ayon => common}/ayon_api/thumbnails.py | 0 .../python/{ayon => common}/ayon_api/utils.py | 0 .../python/{ayon => common}/ayon_api/version.py | 0 16 files changed, 11 insertions(+), 20 deletions(-) rename openpype/vendor/python/{ayon => common}/ayon_api/__init__.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/_api.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/constants.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/entity_hub.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/events.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/exceptions.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/graphql.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/graphql_queries.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/operations.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/server_api.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/thumbnails.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/utils.py (100%) rename openpype/vendor/python/{ayon => common}/ayon_api/version.py (100%) diff --git a/ayon_start.py b/ayon_start.py index 1e791f4f4f..11677b4415 100644 --- a/ayon_start.py +++ b/ayon_start.py @@ -96,19 +96,6 @@ else: sys.path.append(_dependencies_path) _python_paths.append(_dependencies_path) -# ------------------------------------------------- -# Temporary solution to add ayon_api to python path -# ------------------------------------------------- -# This is to avoid need of new build & release when ayon-python-api is updated. -ayon_dependency_dir = os.path.join( - AYON_ROOT, "openpype", "vendor", "python", "ayon" -) -if ayon_dependency_dir in _python_paths: - _python_paths.remove(ayon_dependency_dir) -_python_paths.insert(0, _dependencies_path) -sys.path.insert(0, ayon_dependency_dir) -# ------------------------------------------------- - # Vendored python modules that must not be in PYTHONPATH environment but # are required for OpenPype processes sys.path.insert(0, os.path.join(AYON_ROOT, "vendor", "python")) diff --git a/common/ayon_common/connection/ui/login_window.py b/common/ayon_common/connection/ui/login_window.py index f2604f0466..d7c0558eec 100644 --- a/common/ayon_common/connection/ui/login_window.py +++ b/common/ayon_common/connection/ui/login_window.py @@ -1,6 +1,6 @@ import traceback -from Qt import QtWidgets, QtCore, QtGui +from qtpy import QtWidgets, QtCore, QtGui from ayon_api.exceptions import UrlError from ayon_api.utils import validate_url, login_to_server @@ -171,7 +171,7 @@ class ServerLoginWindow(QtWidgets.QDialog): password_label = QtWidgets.QLabel("Password:", user_cred_widget) password_input = PlaceholderLineEdit(user_cred_widget) password_input.setPlaceholderText("< *********** >") - password_input.setEchoMode(password_input.Password) + password_input.setEchoMode(PlaceholderLineEdit.Password) api_label = QtWidgets.QLabel("API key:", user_cred_widget) api_preview = QtWidgets.QLineEdit(user_cred_widget) @@ -405,14 +405,18 @@ class ServerLoginWindow(QtWidgets.QDialog): def _center_window(self): """Move window to center of screen.""" - desktop = QtWidgets.QApplication.desktop() - screen_idx = desktop.screenNumber(self) - screen_geo = desktop.screenGeometry(screen_idx) + if hasattr(QtWidgets.QApplication, "desktop"): + desktop = QtWidgets.QApplication.desktop() + screen_idx = desktop.screenNumber(self) + screen_geo = desktop.screenGeometry(screen_idx) + else: + screen = self.screen() + screen_geo = screen.geometry() + geo = self.frameGeometry() geo.moveCenter(screen_geo.center()) if geo.y() < screen_geo.y(): geo.setY(screen_geo.y()) - self.move(geo.topLeft()) def _on_url_change(self, text): diff --git a/common/ayon_common/connection/ui/widgets.py b/common/ayon_common/connection/ui/widgets.py index 04c6a8e5f2..78b73e056d 100644 --- a/common/ayon_common/connection/ui/widgets.py +++ b/common/ayon_common/connection/ui/widgets.py @@ -1,4 +1,4 @@ -from Qt import QtWidgets, QtCore, QtGui +from qtpy import QtWidgets, QtCore, QtGui class PressHoverButton(QtWidgets.QPushButton): diff --git a/openpype/vendor/python/ayon/ayon_api/__init__.py b/openpype/vendor/python/common/ayon_api/__init__.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/__init__.py rename to openpype/vendor/python/common/ayon_api/__init__.py diff --git a/openpype/vendor/python/ayon/ayon_api/_api.py b/openpype/vendor/python/common/ayon_api/_api.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/_api.py rename to openpype/vendor/python/common/ayon_api/_api.py diff --git a/openpype/vendor/python/ayon/ayon_api/constants.py b/openpype/vendor/python/common/ayon_api/constants.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/constants.py rename to openpype/vendor/python/common/ayon_api/constants.py diff --git a/openpype/vendor/python/ayon/ayon_api/entity_hub.py b/openpype/vendor/python/common/ayon_api/entity_hub.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/entity_hub.py rename to openpype/vendor/python/common/ayon_api/entity_hub.py diff --git a/openpype/vendor/python/ayon/ayon_api/events.py b/openpype/vendor/python/common/ayon_api/events.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/events.py rename to openpype/vendor/python/common/ayon_api/events.py diff --git a/openpype/vendor/python/ayon/ayon_api/exceptions.py b/openpype/vendor/python/common/ayon_api/exceptions.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/exceptions.py rename to openpype/vendor/python/common/ayon_api/exceptions.py diff --git a/openpype/vendor/python/ayon/ayon_api/graphql.py b/openpype/vendor/python/common/ayon_api/graphql.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/graphql.py rename to openpype/vendor/python/common/ayon_api/graphql.py diff --git a/openpype/vendor/python/ayon/ayon_api/graphql_queries.py b/openpype/vendor/python/common/ayon_api/graphql_queries.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/graphql_queries.py rename to openpype/vendor/python/common/ayon_api/graphql_queries.py diff --git a/openpype/vendor/python/ayon/ayon_api/operations.py b/openpype/vendor/python/common/ayon_api/operations.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/operations.py rename to openpype/vendor/python/common/ayon_api/operations.py diff --git a/openpype/vendor/python/ayon/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/server_api.py rename to openpype/vendor/python/common/ayon_api/server_api.py diff --git a/openpype/vendor/python/ayon/ayon_api/thumbnails.py b/openpype/vendor/python/common/ayon_api/thumbnails.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/thumbnails.py rename to openpype/vendor/python/common/ayon_api/thumbnails.py diff --git a/openpype/vendor/python/ayon/ayon_api/utils.py b/openpype/vendor/python/common/ayon_api/utils.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/utils.py rename to openpype/vendor/python/common/ayon_api/utils.py diff --git a/openpype/vendor/python/ayon/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py similarity index 100% rename from openpype/vendor/python/ayon/ayon_api/version.py rename to openpype/vendor/python/common/ayon_api/version.py From c10781a662e9dd85839d8eb786e471b0709cbccf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 4 Apr 2023 10:29:39 +0200 Subject: [PATCH 210/446] General: Reduce usage of legacy io (#4723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * General: Connect to AYON server (base) (#3924) * implemented 'get_workfile_info' in entities * removed 'prepare_asset_update_data' which is not used * disable settings and project manager if in v4 mode * prepared conversion helper functions for v4 entities * prepared conversion functions for hero versions * fix hero versions * implemented get_archived_representations * fix get latest versions * return prepared changes * handle archived representation * raise exception on failed json conversion * map archived to active properly * make sure default fields are added * fix conversion of hero version entity * fix conversion of archived representations * fix some conversions of representations and versions * changed active behavior in queries * fixed hero versions * implemented basic thumbnail caching * added raw variants of crud methods * implemented methods to get and create thumbnail * fix from flat dict * implemented some basic folder conversion for updates * fix thumbnail updates for version * implemented v4 thumbnail integrator * simplified data mapping * 'get_thumbnail' function also expect entity type and entity id for which is the thumbnail received * implemented 'get_thumbnail' for server * fix how thumbnail id is received from entity * removed unnecessary method 'get_thumbnail_id_from_source' * implemented thumbnail resolver for v4 * removed unnecessary print * move create and delete project directly to server api * disable local settings action too on v4 * OP-3521 - added method to check and download updated addons from v4 server * OP-3521 - added more descriptive error message for missing source * OP-3521 - added default implementation of addon downloader to import * OP-3521 - added check for dependency package zips WIP - server doesn't contain required endpoint. Testing only with mockup data for now. * OP-3521 - fixed parsing of DependencyItem Added Server Url type and ServerAddonDownloader - v4 server doesn't know its own DNS for static files so it is sending unique name and url must be created during runtime. * OP-3521 - fixed creation of targed directories * change nev keys to look for and don't set them automatically * fix task type conversion * implemented base of loading v4 addons in v3 * Refactored argument name in Downloaders * Updated parsing to DependencyItem according to current schema * Implemented downloading of package from server * Updated resolving of failures Uses Enum items. * Introduced passing of authorization token Better to inject it than to have it from env var. * Remove weird parsing of server_url Not necessary, endpoints have same prefix. * Fix doubling asset version name in addons folder Zip file should already contain `addonName_addonVersion` as first subfolder * Fix doubling asset version name in addons folder Zip file should already contain `addonName_addonVersion` as first subfolder * Made server_endpoint optional Argument should be better for testing, but for calling from separate methods it would be better to encapsulate it. Removed unwanted temporary productionPackage value * Use existing method to pull addon info from Server to load v4 version of addon * Raise exception when server doesn't have any production dependency package * added ability to specify v3 alias of addon name * expect v3_alias as uppered constant * Re-implemented method to get addon info Previous implementation wouldn't work in Python2 hosts. Will be refactored in the future. * fix '__getattr__' * added ayon api to pyproject.toml and lock file * use ayon api in common connection * added mapping for label * use ayon_api in client codebase * separated clearing cache of url and username * bump ayon api version * rename env 'OP4_TEST' to 'USE_AYON_SERVER' * Move and renamend get_addons_info to get_addons_info_as_dict in addon_distribution Should be moved to ayon_api later * Replaced requests calls with ayon_api * Replaced OP4_TEST_ENABLED with AYON_SERVER_ENABLED fixed endpoints * Hound * Hound * OP-3521 - fix wrong key in get_representation_parents parents overloads parents * OP-3521 - changes for v4 of SiteSync addon * OP-3521 - fix names * OP-3521 - remove storing project_name It should be safer to go thorug self.dbcon apparently * OP-3521 - remove unwanted "context["folder"]" can be only in dummy test data * OP-3521 - move site sync loaders to addon * Use only project instead of self.project * OP-3521 - added missed get_progress_for_repre * base of settings conversion script * simplified ayon functions in start.py * added loading of settings from ayon server * added a note about colors * fix global and local settings functions * AvalonMongoDB is not using mongo connection on ayon server enabled * 'get_dynamic_modules_dirs' is not checking system settings for paths in setting * log viewer is disabled when ayon server is enabled * basic logic of enabling/disabled addons * don't use mongo logging if ayon server is enabled * update ayon api * bump ayon api again * use ayon_api to get addons info in modules/base * update ayon api * moved helper functions to get addons and dependencies dir to common functions * Initialization of AddonInfo is not crashing on unkonwn sources * renamed 'DependencyDownloader' to 'AyonServerDownloader' * renamed function 'default_addon_downloader' to 'get_default_addon_downloader' * Added ability to convert 'WebAddonSource' to 'ServerResourceSorce' * missing dependency package on server won't cause crash * data sent to downloaders don't contain ayon specific headers * modified addon distribution to not duplicate 'ayon_api' functionality * fix doubled function defintioin * unzip client file to addon destination * formatting - unify quotes * disable usage of mongo connection if in ayon mode * renamed window.py to login_window.py * added webpublisher settings conversion * added maya conversion function * reuse variable * reuse variable (similar to previous commit) * fix ayon addons loading * fix typo 'AyonSettingsCahe' -> 'AyonSettingsCache' * fix enabled state changes * fix rr_path in royal render conversion * avoid mongo calls in AYON state * implemented custom AYON start script * fix formatting (after black) * ayon_start cleanup * 'get_addons_dir' and 'get_dependencies_dir' store value to environment variable * add docstrings to local dir functions * addon info has full name * fix modules enabled states * removed unused 'run_disk_mapping_commands' * removed ayon logic from 'start.py' * fix warning message * renamed 'openpype_common' to 'ayon_common' * removed unused import * don't import igniter * removed startup validations of third parties * change what's shown in version info * fix which keys are applied from ayon values * fix method name * get applications from attribs * Implemented UI basics to be able change user or logout * merged server.py and credentials.py * add more metadata to urls * implemented change token * implemented change user ui functionality * implemented change user ui * modify window to handle username and token value * pass username to add server * fix show UI cases * added loggin action to tray * update ayon api * added missing dependency * convert applications to config in a right way * initial implementation of 'nuke' settings conversion * removed few nuke comments * implemented hiero conversion * added imageio conversion * added run ayon tray script * fix few settings conversions * Renamed class of source classes as they are not just for addons * implemented objec to track source transfer progress * Implemented distribution item with multiple sources * Implemented ayon distribution wrapper to care about multiple things during distribution * added 'cleanup' method for downlaoders * download gets tranfer progress object * Change UploadState enum * added missing imports * use AyonDistribution in ayon_start.py * removed unused functions * removed implemented TODOs * fix import * fix key used for Web source * removed temp development fix * formatting fix * keep information if source require distribution * handle 'require_distribution' attribute in distribution process * added path attribute to server source * added option to pass addons infor to ayon distribution * fix tests * fix formatting * Fix typo * Fix typo * remove '_try_convert_to_server_source' * renamed attributes and methods to match their content * it is possible to pass dependency package info to AyonDistribution * fix called methods in tests * added public properties for error message and error detail * Added filename to WebSourceInfo Useful for GDrive sharable links where target file name is unknown/unparsable, it should be provided explicitly. * unify source conversion by adding 'convert_source' function * Fix error message Co-authored-by: Roy Nieterau * added docstring for 'transfer_progress' * don't create metadata file on read * added few docstrings * add default folder fields to folder/task queries * fix generators * add dependencies when runnign from code * add sys paths from distribution to pythonpath env * fix missing applications * added missing conversions for maya renderers * fix formatting * update ayon api * fix hashes in lock file * Use better exception Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> * Use Python 3 syntax Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> * apply some of sugested changes in ayon_start * added some docstrings and suggested modifications * copy create env from develop * fix rendersettings conversion * change code by suggestions * added missing args to docstring * added missing docstrings * separated downloader and download factory * fix ayon settings * added some basic file docstring to ayon_settings * join else conditions * fix project settings conversion * fix created at conversion * fix workfile info query * fix publisher UI * added utils function 'get_ayon_appdirs' * fix 'get_all_current_info' * fix server url assignment when url is set * updated ayon api * added utils functions to create local site id for ayon * added helper functions to create global connection * create global connection in ayon start to start use site id * use ayon site id in ayon mode * formatting cleanup * added header docstring * fixes after ayon_api update * load addons from ynput appdirs * fix function call * added docstring * update ayon pyton api * fix settings access * use ayon_api to get root overrides in Anatomy * bumbayon version to 0.1.13 * nuke: fixing settings keys from settings * fix burnins definitions * change v4 to AYON in thumbnail integrate * fix one more v4 information * Fixes after rebase * fix extract burnin conversion * additional fix of extract burnin * SiteSync:added missed loaders or v3 compatibility (#4587) * Added site sync loaders for v3 compatibility * Fix get_progress_for_repre * use 'files.name' instead of 'files.baseName' * update ayon api to 0.1.14 * add common to include files * change arguments for hero version creation * skip shotgrid settings conversion if different ayon addon is used * added ayon icons * fix labels of application variants * added option to show login window always on top * login window on invalid credentials is always on top * update ayon api * update ayon api * add entityType to project and folders * AYON: Editorial hierarchy creation (#4699) * disable extract hierarchy avalon when ayon mode is enabled * implemented extract hierarchy to AYON --------- Co-authored-by: Petr Kalis Co-authored-by: Roy Nieterau Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> Co-authored-by: Jakub Jezek * replace 'legacy_io' with context functions in load plugins * added 'get_global_context' to pipeline init * use context getters instead of legacy_io in publish plugins * use data on context instead of 'legacy_io' in submit publish job * skip query of asset docs in collect nuke reads * use context functions on other places * 'list_looks' expects project name * remove 'get_context_title' * don't pass AvalonMongoDB to prelaunch hooks * change how context is calculated in hiero * implemented function 'get_fps_for_current_context' for maya * initialize '_image_dir' and '_image_prefixes' in init * legacy creator is using 'get_current_project_name' * fill docstrings * use context functions in workfile builders * hound fixes * 'create_workspace_mel' can expect project settings * swapped order of arguments * use information from instance/context data * Use self.project_name in workfiles tool Co-authored-by: Roy Nieterau * Remove outdated todo Co-authored-by: Roy Nieterau * don't query project document in nuke lib * Fix access to context data * Use right function to get project name Co-authored-by: Roy Nieterau * fix submit max deadline and swap order of arguments * added 'get_context_label' to nuke * fix import * fix typo 'curent_context' -> 'current_context' * fix project_setting variable * fix submit publish job environments * use task from context * Removed unused import --------- Co-authored-by: Petr Kalis Co-authored-by: Roy Nieterau Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> Co-authored-by: Jakub Jezek --- openpype/hooks/pre_global_host_data.py | 10 +-- openpype/host/dirmap.py | 2 +- .../plugins/publish/collect_workfile.py | 3 +- .../publish/validate_instance_asset.py | 6 +- openpype/hosts/blender/api/ops.py | 12 ++-- openpype/hosts/blender/api/pipeline.py | 6 +- .../blender/plugins/create/create_action.py | 4 +- .../plugins/create/create_animation.py | 4 +- .../blender/plugins/create/create_camera.py | 4 +- .../blender/plugins/create/create_layout.py | 4 +- .../blender/plugins/create/create_model.py | 4 +- .../plugins/create/create_pointcache.py | 4 +- .../blender/plugins/create/create_review.py | 4 +- .../blender/plugins/create/create_rig.py | 4 +- .../plugins/publish/collect_current_file.py | 6 +- .../blender/plugins/publish/collect_review.py | 3 +- .../publish/collect_celaction_instances.py | 3 +- openpype/hosts/flame/api/menu.py | 10 +-- .../plugins/publish/collect_timeline_otio.py | 3 +- openpype/hosts/fusion/api/lib.py | 4 +- openpype/hosts/fusion/api/menu.py | 4 +- .../deploy/Scripts/Comp/OpenPype/switch_ui.py | 4 +- .../fusion/plugins/create/create_workfile.py | 9 ++- openpype/hosts/harmony/api/README.md | 2 +- .../plugins/publish/collect_farm_render.py | 5 +- .../plugins/publish/collect_palettes.py | 4 +- .../plugins/publish/collect_workfile.py | 2 +- .../plugins/publish/extract_template.py | 2 +- .../plugins/publish/validate_instances.py | 7 +- .../publish/validate_scene_settings.py | 4 +- openpype/hosts/hiero/api/lib.py | 14 ++-- openpype/hosts/hiero/api/menu.py | 30 +++++---- openpype/hosts/hiero/api/tags.py | 4 +- .../hosts/hiero/plugins/load/load_clip.py | 6 +- .../hosts/hiero/plugins/load/load_effects.py | 6 +- .../plugins/publish/precollect_workfile.py | 3 +- .../collect_assetbuilds.py | 3 +- openpype/hosts/houdini/api/lib.py | 10 +-- openpype/hosts/houdini/api/shelves.py | 4 +- .../houdini/plugins/create/create_hda.py | 3 +- .../houdini/plugins/create/create_workfile.py | 7 +- .../plugins/publish/collect_usd_bootstrap.py | 3 +- .../plugins/publish/extract_usd_layered.py | 3 +- .../validate_usd_shade_model_exists.py | 3 +- .../avalon_uri_processor.py | 4 +- openpype/hosts/max/api/lib_renderproducts.py | 9 ++- openpype/hosts/max/api/lib_rendersettings.py | 4 +- .../max/plugins/publish/collect_workfile.py | 5 +- .../plugins/publish/validate_pointcloud.py | 15 ++--- openpype/hosts/maya/api/action.py | 3 +- openpype/hosts/maya/api/commands.py | 6 +- openpype/hosts/maya/api/lib.py | 67 +++++++++++-------- openpype/hosts/maya/api/lib_rendersettings.py | 50 +++++++------- openpype/hosts/maya/api/menu.py | 29 ++++---- openpype/hosts/maya/api/pipeline.py | 15 +++-- openpype/hosts/maya/api/setdress.py | 8 +-- openpype/hosts/maya/hooks/pre_copy_mel.py | 5 +- openpype/hosts/maya/lib.py | 7 +- .../plugins/inventory/import_modelrender.py | 6 +- .../hosts/maya/plugins/load/load_audio.py | 4 +- .../hosts/maya/plugins/load/load_gpucache.py | 3 +- .../hosts/maya/plugins/load/load_image.py | 5 +- .../maya/plugins/load/load_image_plane.py | 6 +- openpype/hosts/maya/plugins/load/load_look.py | 4 +- .../maya/plugins/load/load_redshift_proxy.py | 3 +- .../hosts/maya/plugins/load/load_reference.py | 6 +- .../maya/plugins/load/load_vdb_to_arnold.py | 3 +- .../maya/plugins/load/load_vdb_to_redshift.py | 3 +- .../maya/plugins/load/load_vdb_to_vray.py | 3 +- .../hosts/maya/plugins/load/load_vrayproxy.py | 9 +-- .../hosts/maya/plugins/load/load_vrayscene.py | 3 +- .../maya/plugins/load/load_yeti_cache.py | 3 +- .../maya/plugins/publish/extract_layout.py | 4 +- .../plugins/publish/submit_maya_muster.py | 6 +- openpype/hosts/maya/startup/userSetup.py | 18 ++--- .../hosts/maya/tools/mayalookassigner/app.py | 6 +- .../maya/tools/mayalookassigner/commands.py | 28 +++++--- .../tools/mayalookassigner/vray_proxies.py | 4 +- openpype/hosts/nuke/api/lib.py | 29 +++++--- openpype/hosts/nuke/api/pipeline.py | 17 +++-- openpype/hosts/nuke/api/plugin.py | 4 +- .../nuke/plugins/create/workfile_creator.py | 9 ++- .../hosts/nuke/plugins/load/load_backdrop.py | 4 +- .../nuke/plugins/load/load_camera_abc.py | 6 +- openpype/hosts/nuke/plugins/load/load_clip.py | 4 +- .../hosts/nuke/plugins/load/load_effects.py | 4 +- .../nuke/plugins/load/load_effects_ip.py | 4 +- .../hosts/nuke/plugins/load/load_gizmo.py | 4 +- .../hosts/nuke/plugins/load/load_gizmo_ip.py | 4 +- .../hosts/nuke/plugins/load/load_image.py | 4 +- .../hosts/nuke/plugins/load/load_model.py | 6 +- .../nuke/plugins/load/load_script_precomp.py | 4 +- .../nuke/plugins/publish/collect_reads.py | 11 +-- openpype/hosts/photoshop/api/pipeline.py | 11 --- .../publish/validate_instance_asset.py | 8 +-- .../hosts/resolve/plugins/load/load_clip.py | 6 +- .../plugins/publish/precollect_workfile.py | 4 +- .../publish/validate_texture_workfiles.py | 39 +++++------ .../tvpaint/plugins/load/load_workfile.py | 9 +-- .../hosts/unreal/plugins/load/load_camera.py | 6 +- .../hosts/unreal/plugins/load/load_layout.py | 6 +- .../plugins/load/load_layout_existing.py | 4 +- .../unreal/plugins/publish/extract_layout.py | 4 +- openpype/lib/applications.py | 11 +-- openpype/lib/usdlib.py | 10 +-- .../plugins/publish/submit_max_deadline.py | 10 ++- .../submit_maya_remote_publish_deadline.py | 4 +- .../plugins/publish/submit_publish_job.py | 29 ++++---- .../plugins/publish/collect_ftrack_api.py | 8 +-- .../publish/collect_sequences_from_job.py | 2 +- .../publish/collect_shotgrid_entities.py | 2 +- openpype/pipeline/__init__.py | 2 + openpype/pipeline/context_tools.py | 22 +++++- openpype/pipeline/create/creator_plugins.py | 4 +- openpype/pipeline/template_data.py | 2 +- openpype/pipeline/workfile/build_workfile.py | 27 +++++--- .../workfile/workfile_template_builder.py | 36 +++++++--- .../publish/collect_current_context.py | 16 ++--- openpype/pype_commands.py | 17 +++-- openpype/scripts/fusion_switch_shot.py | 5 +- openpype/tools/adobe_webserver/app.py | 9 +-- openpype/tools/creator/window.py | 14 ++-- openpype/tools/sceneinventory/model.py | 6 +- openpype/tools/utils/host_tools.py | 4 +- openpype/tools/utils/lib.py | 16 +++-- openpype/tools/workfiles/files_widget.py | 3 +- openpype/tools/workfiles/window.py | 21 +++--- poetry.lock | 18 +++++ pyproject.toml | 1 + 129 files changed, 598 insertions(+), 508 deletions(-) diff --git a/openpype/hooks/pre_global_host_data.py b/openpype/hooks/pre_global_host_data.py index 8a178915fb..260e28a18b 100644 --- a/openpype/hooks/pre_global_host_data.py +++ b/openpype/hooks/pre_global_host_data.py @@ -5,7 +5,7 @@ from openpype.lib import ( prepare_app_environments, prepare_context_environments ) -from openpype.pipeline import AvalonMongoDB, Anatomy +from openpype.pipeline import Anatomy class GlobalHostDataHook(PreLaunchHook): @@ -26,7 +26,6 @@ class GlobalHostDataHook(PreLaunchHook): "app": app, - "dbcon": self.data["dbcon"], "project_doc": self.data["project_doc"], "asset_doc": self.data["asset_doc"], @@ -62,13 +61,6 @@ class GlobalHostDataHook(PreLaunchHook): # Anatomy self.data["anatomy"] = Anatomy(project_name) - # Mongo connection - dbcon = AvalonMongoDB() - dbcon.Session["AVALON_PROJECT"] = project_name - dbcon.install() - - self.data["dbcon"] = dbcon - # Project document project_doc = get_project(project_name) self.data["project_doc"] = project_doc diff --git a/openpype/host/dirmap.py b/openpype/host/dirmap.py index 42bf80ecec..e77f06e9d6 100644 --- a/openpype/host/dirmap.py +++ b/openpype/host/dirmap.py @@ -149,7 +149,7 @@ class HostDirmap(object): Returns: dict : { "source-path": [XXX], "destination-path": [YYYY]} """ - project_name = os.getenv("AVALON_PROJECT") + project_name = self.project_name mapping = {} if (not self.sync_module.enabled or diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index c21c3623c3..dc557f67fc 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -1,7 +1,6 @@ import os import pyblish.api -from openpype.pipeline import legacy_io from openpype.pipeline.create import get_subset_name @@ -44,7 +43,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): instance.data["publish"] = instance.data["active"] # for DL def _get_new_instance(self, context, scene_file): - task = legacy_io.Session["AVALON_TASK"] + task = context.data["task"] version = context.data["version"] asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py index 6c36136b20..36f6035d23 100644 --- a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py @@ -1,6 +1,6 @@ import pyblish.api -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_asset_name from openpype.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, @@ -30,7 +30,7 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): for instance in instances: data = stub.read(instance[0]) - data["asset"] = legacy_io.Session["AVALON_ASSET"] + data["asset"] = get_current_asset_name() stub.imprint(instance[0].instance_id, data) @@ -54,7 +54,7 @@ class ValidateInstanceAsset(pyblish.api.InstancePlugin): def process(self, instance): instance_asset = instance.data["asset"] - current_asset = legacy_io.Session["AVALON_ASSET"] + current_asset = get_current_asset_name() msg = ( f"Instance asset {instance_asset} is not the same " f"as current context {current_asset}." diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 91cbfe524f..4ce83dbfe9 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -16,7 +16,7 @@ import bpy import bpy.utils.previews from openpype import style -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_asset_name, get_current_task_name from openpype.tools.utils import host_tools from .workio import OpenFileCacher @@ -283,7 +283,7 @@ class LaunchLoader(LaunchQtApp): def before_window_show(self): self._window.set_context( - {"asset": legacy_io.Session["AVALON_ASSET"]}, + {"asset": get_current_asset_name()}, refresh=True ) @@ -331,8 +331,8 @@ class LaunchWorkFiles(LaunchQtApp): def execute(self, context): result = super().execute(context) self._window.set_context({ - "asset": legacy_io.Session["AVALON_ASSET"], - "task": legacy_io.Session["AVALON_TASK"] + "asset": get_current_asset_name(), + "task": get_current_task_name() }) return result @@ -362,8 +362,8 @@ class TOPBAR_MT_avalon(bpy.types.Menu): else: pyblish_menu_icon_id = 0 - asset = legacy_io.Session['AVALON_ASSET'] - task = legacy_io.Session['AVALON_TASK'] + asset = get_current_asset_name() + task = get_current_task_name() context_label = f"{asset}, {task}" context_label_item = layout.row() context_label_item.operator( diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 0f756d8cb6..eb696ec184 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -14,6 +14,8 @@ from openpype.client import get_asset_by_name from openpype.pipeline import ( schema, legacy_io, + get_current_project_name, + get_current_asset_name, register_loader_plugin_path, register_creator_plugin_path, deregister_loader_plugin_path, @@ -112,8 +114,8 @@ def message_window(title, message): def set_start_end_frames(): - project_name = legacy_io.active_project() - asset_name = legacy_io.Session["AVALON_ASSET"] + project_name = get_current_project_name() + asset_name = get_current_asset_name() asset_doc = get_asset_by_name(project_name, asset_name) scene = bpy.context.scene diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 54b3a501a7..0203ba74c0 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_task_name import openpype.hosts.blender.api.plugin from openpype.hosts.blender.api import lib @@ -22,7 +22,7 @@ class CreateAction(openpype.hosts.blender.api.plugin.Creator): name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - self.data['task'] = legacy_io.Session.get('AVALON_TASK') + self.data['task'] = get_current_task_name() lib.imprint(collection, self.data) if (self.options or {}).get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index a0e9e5e399..bc2840952b 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -37,7 +37,7 @@ class CreateAnimation(plugin.Creator): # asset_group.empty_display_type = 'SINGLE_ARROW' asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - self.data['task'] = legacy_io.Session.get('AVALON_TASK') + self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) if (self.options or {}).get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index ada512d7ac..6defe02fe5 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -35,7 +35,7 @@ class CreateCamera(plugin.Creator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - self.data['task'] = legacy_io.Session.get('AVALON_TASK') + self.data['task'] = get_current_task_name() print(f"self.data: {self.data}") lib.imprint(asset_group, self.data) diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 5949a4b86e..68cfaa41ac 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -34,7 +34,7 @@ class CreateLayout(plugin.Creator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - self.data['task'] = legacy_io.Session.get('AVALON_TASK') + self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index fedc708943..e5204b5b53 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -34,7 +34,7 @@ class CreateModel(plugin.Creator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - self.data['task'] = legacy_io.Session.get('AVALON_TASK') + self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 38707fd3b1..6220f68dc5 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_task_name import openpype.hosts.blender.api.plugin from openpype.hosts.blender.api import lib @@ -22,7 +22,7 @@ class CreatePointcache(openpype.hosts.blender.api.plugin.Creator): name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - self.data['task'] = legacy_io.Session.get('AVALON_TASK') + self.data['task'] = get_current_task_name() lib.imprint(collection, self.data) if (self.options or {}).get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index bf4ea6a7cd..914f249891 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -33,7 +33,7 @@ class CreateReview(plugin.Creator): name = plugin.asset_name(asset, subset) asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - self.data['task'] = legacy_io.Session.get('AVALON_TASK') + self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) if (self.options or {}).get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 0abd306c6b..2e04fb71c1 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -34,7 +34,7 @@ class CreateRig(plugin.Creator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - self.data['task'] = legacy_io.Session.get('AVALON_TASK') + self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/publish/collect_current_file.py b/openpype/hosts/blender/plugins/publish/collect_current_file.py index c3097a0694..c2d8a96a18 100644 --- a/openpype/hosts/blender/plugins/publish/collect_current_file.py +++ b/openpype/hosts/blender/plugins/publish/collect_current_file.py @@ -2,7 +2,7 @@ import os import bpy import pyblish.api -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_task_name, get_current_asset_name from openpype.hosts.blender.api import workio @@ -37,7 +37,7 @@ class CollectBlenderCurrentFile(pyblish.api.ContextPlugin): folder, file = os.path.split(current_file) filename, ext = os.path.splitext(file) - task = legacy_io.Session["AVALON_TASK"] + task = get_current_task_name() data = {} @@ -47,7 +47,7 @@ class CollectBlenderCurrentFile(pyblish.api.ContextPlugin): data.update({ "subset": subset, - "asset": os.getenv("AVALON_ASSET", None), + "asset": get_current_asset_name(), "label": subset, "publish": True, "family": "workfile", diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index d6abd9d967..82b3ca11eb 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -1,7 +1,6 @@ import bpy import pyblish.api -from openpype.pipeline import legacy_io class CollectReview(pyblish.api.InstancePlugin): @@ -39,7 +38,7 @@ class CollectReview(pyblish.api.InstancePlugin): if not instance.data.get("remove"): - task = legacy_io.Session.get("AVALON_TASK") + task = instance.context.data["task"] instance.data.update({ "subset": f"{task}Review", diff --git a/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py b/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py index 35ac7fc264..c815c1edd4 100644 --- a/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py +++ b/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py @@ -1,6 +1,5 @@ import os import pyblish.api -from openpype.pipeline import legacy_io class CollectCelactionInstances(pyblish.api.ContextPlugin): @@ -10,7 +9,7 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.1 def process(self, context): - task = legacy_io.Session["AVALON_TASK"] + task = context.data["task"] current_file = context.data["currentFile"] staging_dir = os.path.dirname(current_file) scene_file = os.path.basename(current_file) diff --git a/openpype/hosts/flame/api/menu.py b/openpype/hosts/flame/api/menu.py index 5f9dc57a61..e8bdf32ebd 100644 --- a/openpype/hosts/flame/api/menu.py +++ b/openpype/hosts/flame/api/menu.py @@ -1,7 +1,9 @@ -import os -from qtpy import QtWidgets from copy import deepcopy from pprint import pformat + +from qtpy import QtWidgets + +from openpype.pipeline import get_current_project_name from openpype.tools.utils.host_tools import HostToolsHelper menu_group_name = 'OpenPype' @@ -61,10 +63,10 @@ class _FlameMenuApp(object): self.framework.prefs_global, self.name) self.mbox = QtWidgets.QMessageBox() - + project_name = get_current_project_name() self.menu = { "actions": [{ - 'name': os.getenv("AVALON_PROJECT", "project"), + 'name': project_name or "project", 'isEnabled': False }], "name": self.menu_group_name diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py index 917041e053..f8cfa9e963 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py @@ -2,7 +2,6 @@ import pyblish.api import openpype.hosts.flame.api as opfapi from openpype.hosts.flame.otio import flame_export -from openpype.pipeline import legacy_io from openpype.pipeline.create import get_subset_name @@ -19,7 +18,7 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin): # main asset_doc = context.data["assetEntity"] - task_name = legacy_io.Session["AVALON_TASK"] + task_name = context.data["task"] project = opfapi.get_current_project() sequence = opfapi.get_current_sequence(opfapi.CTX.selection) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index cba8c38c2f..d96557571b 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -14,7 +14,7 @@ from openpype.client import ( ) from openpype.pipeline import ( switch_container, - legacy_io, + get_current_project_name, ) from openpype.pipeline.context_tools import get_current_project_asset @@ -206,7 +206,7 @@ def switch_item(container, # Collect any of current asset, subset and representation if not provided # so we can use the original name from those. - project_name = legacy_io.active_project() + project_name = get_current_project_name() if any(not x for x in [asset_name, subset_name, representation_name]): repre_id = container["representation"] representation = get_representation_by_id(project_name, repre_id) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 92f38a64c2..50250a6656 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -12,7 +12,7 @@ from openpype.hosts.fusion.api.lib import ( set_asset_framerange, set_asset_resolution, ) -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_asset_name from openpype.resources import get_openpype_icon_filepath from .pipeline import FusionEventHandler @@ -125,7 +125,7 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_task_changed(self): # Update current context label - label = legacy_io.Session["AVALON_ASSET"] + label = get_current_asset_name() self.asset_label.setText(label) def register_callback(self, name, fn): diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py index f08dc0bf2c..87322235f5 100644 --- a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py +++ b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py @@ -11,7 +11,7 @@ from openpype.client import get_assets from openpype import style from openpype.pipeline import ( install_host, - legacy_io, + get_current_project_name, ) from openpype.hosts.fusion import api from openpype.pipeline.context_tools import get_workdir_from_session @@ -167,7 +167,7 @@ class App(QtWidgets.QWidget): return items def collect_asset_names(self): - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset_docs = get_assets(project_name, fields=["name"]) asset_names = { asset_doc["name"] diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 40721ea88a..8acaaa172f 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -5,7 +5,6 @@ from openpype.client import get_asset_by_name from openpype.pipeline import ( AutoCreator, CreatedInstance, - legacy_io, ) @@ -64,10 +63,10 @@ class FusionWorkfileCreator(AutoCreator): existing_instance = instance break - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - host_name = legacy_io.Session["AVALON_APP"] + project_name = self.create_context.get_current_project_name() + asset_name = self.create_context.get_current_asset_name() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/harmony/api/README.md b/openpype/hosts/harmony/api/README.md index 12f21f551a..be3920fe29 100644 --- a/openpype/hosts/harmony/api/README.md +++ b/openpype/hosts/harmony/api/README.md @@ -610,7 +610,7 @@ class ImageSequenceLoader(load.LoaderPlugin): def update(self, container, representation): node = container.pop("node") - project_name = legacy_io.active_project() + project_name = get_current_project_name() version = get_version_by_id(project_name, representation["parent"]) files = [] for f in version["data"]["files"]: diff --git a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py index f6b26eb3e8..5e9b9094a7 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py +++ b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py @@ -5,7 +5,6 @@ from pathlib import Path import attr from openpype.lib import get_formatted_current_time -from openpype.pipeline import legacy_io from openpype.pipeline import publish from openpype.pipeline.publish import RenderInstance import openpype.hosts.harmony.api as harmony @@ -99,6 +98,8 @@ class CollectFarmRender(publish.AbstractCollectRender): self_name = self.__class__.__name__ + asset_name = context.data["asset"] + for node in context.data["allNodes"]: data = harmony.read(node) @@ -141,7 +142,7 @@ class CollectFarmRender(publish.AbstractCollectRender): source=context.data["currentFile"], label=node.split("/")[1], subset=subset_name, - asset=legacy_io.Session["AVALON_ASSET"], + asset=asset_name, task=task_name, attachTo=False, setMembers=[node], diff --git a/openpype/hosts/harmony/plugins/publish/collect_palettes.py b/openpype/hosts/harmony/plugins/publish/collect_palettes.py index bbd60d1c55..e19057e302 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_palettes.py +++ b/openpype/hosts/harmony/plugins/publish/collect_palettes.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Collect palettes from Harmony.""" -import os import json import re @@ -32,6 +31,7 @@ class CollectPalettes(pyblish.api.ContextPlugin): if (not any([re.search(pattern, task_name) for pattern in self.allowed_tasks])): return + asset_name = context.data["asset"] for name, id in palettes.items(): instance = context.create_instance(name) @@ -39,7 +39,7 @@ class CollectPalettes(pyblish.api.ContextPlugin): "id": id, "family": "harmony.palette", 'families': [], - "asset": os.environ["AVALON_ASSET"], + "asset": asset_name, "subset": "{}{}".format("palette", name) }) self.log.info( diff --git a/openpype/hosts/harmony/plugins/publish/collect_workfile.py b/openpype/hosts/harmony/plugins/publish/collect_workfile.py index 3624147435..4492ab37a5 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_workfile.py +++ b/openpype/hosts/harmony/plugins/publish/collect_workfile.py @@ -36,5 +36,5 @@ class CollectWorkfile(pyblish.api.ContextPlugin): "family": family, "families": [family], "representations": [], - "asset": os.environ["AVALON_ASSET"] + "asset": context.data["asset"] }) diff --git a/openpype/hosts/harmony/plugins/publish/extract_template.py b/openpype/hosts/harmony/plugins/publish/extract_template.py index 458bf25a3c..e75459fe1e 100644 --- a/openpype/hosts/harmony/plugins/publish/extract_template.py +++ b/openpype/hosts/harmony/plugins/publish/extract_template.py @@ -75,7 +75,7 @@ class ExtractTemplate(publish.Extractor): instance.data["representations"] = [representation] instance.data["version_name"] = "{}_{}".format( - instance.data["subset"], os.environ["AVALON_TASK"]) + instance.data["subset"], instance.context.data["task"]) def get_backdrops(self, node: str) -> list: """Get backdrops for the node. diff --git a/openpype/hosts/harmony/plugins/publish/validate_instances.py b/openpype/hosts/harmony/plugins/publish/validate_instances.py index ac367082ef..7183de6048 100644 --- a/openpype/hosts/harmony/plugins/publish/validate_instances.py +++ b/openpype/hosts/harmony/plugins/publish/validate_instances.py @@ -1,8 +1,7 @@ -import os - import pyblish.api import openpype.hosts.harmony.api as harmony +from openpype.pipeline import get_current_asset_name from openpype.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, @@ -30,7 +29,7 @@ class ValidateInstanceRepair(pyblish.api.Action): for instance in instances: data = harmony.read(instance.data["setMembers"][0]) - data["asset"] = os.environ["AVALON_ASSET"] + data["asset"] = get_current_asset_name() harmony.imprint(instance.data["setMembers"][0], data) @@ -44,7 +43,7 @@ class ValidateInstance(pyblish.api.InstancePlugin): def process(self, instance): instance_asset = instance.data["asset"] - current_asset = os.environ["AVALON_ASSET"] + current_asset = get_current_asset_name() msg = ( "Instance asset is not the same as current asset:" f"\nInstance: {instance_asset}\nCurrent: {current_asset}" diff --git a/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py b/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py index 6e4c6955e4..866f12076a 100644 --- a/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py @@ -67,7 +67,9 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): expected_settings["frameEndHandle"] = expected_settings["frameEnd"] +\ expected_settings["handleEnd"] - if (any(re.search(pattern, os.getenv('AVALON_TASK')) + task_name = instance.context.data["task"] + + if (any(re.search(pattern, task_name) for pattern in self.skip_resolution_check)): self.log.info("Skipping resolution check because of " "task name and pattern {}".format( diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 09d73f5cc2..bf719160d1 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -22,9 +22,7 @@ except ImportError: from openpype.client import get_project from openpype.settings import get_project_settings -from openpype.pipeline import ( - get_current_project_name, legacy_io, Anatomy -) +from openpype.pipeline import Anatomy, get_current_project_name from openpype.pipeline.load import filter_containers from openpype.lib import Logger from . import tags @@ -626,7 +624,7 @@ def get_publish_attribute(tag): def sync_avalon_data_to_workfile(): # import session to get project dir - project_name = legacy_io.Session["AVALON_PROJECT"] + project_name = get_current_project_name() anatomy = Anatomy(project_name) work_template = anatomy.templates["work"]["path"] @@ -821,7 +819,7 @@ class PublishAction(QtWidgets.QAction): # # create root node and save all metadata # root_node = hiero.core.nuke.RootNode() # -# anatomy = Anatomy(os.environ["AVALON_PROJECT"]) +# anatomy = Anatomy(get_current_project_name()) # work_template = anatomy.templates["work"]["path"] # root_path = anatomy.root_value_for_template(work_template) # @@ -1041,7 +1039,7 @@ def _set_hrox_project_knobs(doc, **knobs): def apply_colorspace_project(): - project_name = os.getenv("AVALON_PROJECT") + project_name = get_current_project_name() # get path the the active projects project = get_current_project(remove_untitled=True) current_file = project.path() @@ -1110,7 +1108,7 @@ def apply_colorspace_project(): def apply_colorspace_clips(): - project_name = os.getenv("AVALON_PROJECT") + project_name = get_current_project_name() project = get_current_project(remove_untitled=True) clips = project.clips() @@ -1264,7 +1262,7 @@ def check_inventory_versions(track_items=None): if not containers: return - project_name = legacy_io.active_project() + project_name = get_current_project_name() filter_result = filter_containers(containers, project_name) for container in filter_result.latest: set_track_color(container["_item"], clip_color_last) diff --git a/openpype/hosts/hiero/api/menu.py b/openpype/hosts/hiero/api/menu.py index 6baeb38cc0..9967e9c875 100644 --- a/openpype/hosts/hiero/api/menu.py +++ b/openpype/hosts/hiero/api/menu.py @@ -4,12 +4,18 @@ import sys import hiero.core from hiero.ui import findMenuAction +from qtpy import QtGui + from openpype.lib import Logger -from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools +from openpype.settings import get_project_settings +from openpype.pipeline import ( + get_current_project_name, + get_current_asset_name, + get_current_task_name +) from . import tags -from openpype.settings import get_project_settings log = Logger.get_logger(__name__) @@ -17,6 +23,13 @@ self = sys.modules[__name__] self._change_context_menu = None +def get_context_label(): + return "{}, {}".format( + get_current_asset_name(), + get_current_task_name() + ) + + def update_menu_task_label(): """Update the task label in Avalon menu to current session""" @@ -27,10 +40,7 @@ def update_menu_task_label(): log.warning("Can't find menuItem: {}".format(object_name)) return - label = "{}, {}".format( - legacy_io.Session["AVALON_ASSET"], - legacy_io.Session["AVALON_TASK"] - ) + label = get_context_label() menu = found_menu.menu() self._change_context_menu = label @@ -43,7 +53,6 @@ def menu_install(): """ - from qtpy import QtGui from . import ( publish, launch_workfiles_app, reload_config, apply_colorspace_project, apply_colorspace_clips @@ -56,10 +65,7 @@ def menu_install(): menu_name = os.environ['AVALON_LABEL'] - context_label = "{0}, {1}".format( - legacy_io.Session["AVALON_ASSET"], - legacy_io.Session["AVALON_TASK"] - ) + context_label = get_context_label() self._change_context_menu = context_label @@ -154,7 +160,7 @@ def add_scripts_menu(): return # load configuration of custom menu - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + project_settings = get_project_settings(get_current_project_name()) config = project_settings["hiero"]["scriptsmenu"]["definition"] _menu = project_settings["hiero"]["scriptsmenu"]["name"] diff --git a/openpype/hosts/hiero/api/tags.py b/openpype/hosts/hiero/api/tags.py index cb7bc14edb..02d8205414 100644 --- a/openpype/hosts/hiero/api/tags.py +++ b/openpype/hosts/hiero/api/tags.py @@ -5,7 +5,7 @@ import hiero from openpype.client import get_project, get_assets from openpype.lib import Logger -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_project_name log = Logger.get_logger(__name__) @@ -142,7 +142,7 @@ def add_tags_to_workfile(): nks_pres_tags = tag_data() # Get project task types. - project_name = legacy_io.active_project() + project_name = get_current_project_name() project_doc = get_project(project_name) tasks = project_doc["config"]["tasks"] nks_pres_tags["[Tasks]"] = {} diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index 71df52f0f8..05bd12d185 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -3,8 +3,8 @@ from openpype.client import ( get_last_version_by_subset_id ) from openpype.pipeline import ( - legacy_io, get_representation_path, + get_current_project_name, ) from openpype.lib.transcoding import ( VIDEO_EXTENSIONS, @@ -148,7 +148,7 @@ class LoadClip(phiero.SequenceLoader): track_item = phiero.get_track_items( track_item_name=namespace).pop() - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) version_data = version_doc.get("data", {}) @@ -211,7 +211,7 @@ class LoadClip(phiero.SequenceLoader): @classmethod def set_item_color(cls, track_item, version_doc): - project_name = legacy_io.active_project() + project_name = get_current_project_name() last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index 4b86149166..31147d013f 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -9,8 +9,8 @@ from openpype.client import ( from openpype.pipeline import ( AVALON_CONTAINER_ID, load, - legacy_io, - get_representation_path + get_representation_path, + get_current_project_name ) from openpype.hosts.hiero import api as phiero from openpype.lib import Logger @@ -168,7 +168,7 @@ class LoadEffects(load.LoaderPlugin): namespace = container['namespace'] # get timeline in out data - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) version_data = version_doc["data"] clip_in = version_data["clipIn"] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 1f477c1639..5a66581531 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -7,7 +7,6 @@ from qtpy.QtGui import QPixmap import hiero.ui -from openpype.pipeline import legacy_io from openpype.hosts.hiero.api.otio import hiero_export @@ -19,7 +18,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): def process(self, context): - asset = legacy_io.Session["AVALON_ASSET"] + asset = context.data["asset"] subset = "workfile" active_timeline = hiero.ui.activeSequence() project = active_timeline.project() diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py index 5f96533052..767f7c30f7 100644 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py +++ b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py @@ -1,6 +1,5 @@ from pyblish import api from openpype.client import get_assets -from openpype.pipeline import legacy_io class CollectAssetBuilds(api.ContextPlugin): @@ -18,7 +17,7 @@ class CollectAssetBuilds(api.ContextPlugin): hosts = ["hiero"] def process(self, context): - project_name = legacy_io.active_project() + project_name = context.data["projectName"] asset_builds = {} for asset in get_assets(project_name): if asset["data"]["entityType"] == "AssetBuild": diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index a32e9d8d61..b03f8c8fc1 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -10,7 +10,7 @@ import json import six from openpype.client import get_asset_by_name -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_project_name, get_current_asset_name from openpype.pipeline.context_tools import get_current_project_asset import hou @@ -78,8 +78,8 @@ def generate_ids(nodes, asset_id=None): """ if asset_id is None: - project_name = legacy_io.active_project() - asset_name = legacy_io.Session["AVALON_ASSET"] + project_name = get_current_project_name() + asset_name = get_current_asset_name() # Get the asset ID from the database for the asset of current context asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) @@ -474,8 +474,8 @@ def maintained_selection(): def reset_framerange(): """Set frame range to current asset""" - project_name = legacy_io.active_project() - asset_name = legacy_io.Session["AVALON_ASSET"] + project_name = get_current_project_name() + asset_name = get_current_asset_name() # Get the asset ID from the database for the asset of current context asset_doc = get_asset_by_name(project_name, asset_name) asset_data = asset_doc["data"] diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 6e0f367f62..21e44e494a 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -4,6 +4,7 @@ import logging import platform from openpype.settings import get_project_settings +from openpype.pipeline import get_current_project_name import hou @@ -17,7 +18,8 @@ def generate_shelves(): current_os = platform.system().lower() # load configuration of houdini shelves - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + project_name = get_current_project_name() + project_settings = get_project_settings(project_name) shelves_set_config = project_settings["houdini"]["shelves"] if not shelves_set_config: diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index 5f95b2efb4..c4093bfbc6 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -4,7 +4,6 @@ from openpype.client import ( get_asset_by_name, get_subsets, ) -from openpype.pipeline import legacy_io from openpype.hosts.houdini.api import plugin @@ -21,7 +20,7 @@ class CreateHDA(plugin.HoudiniCreator): # type: (str) -> bool """Check if existing subset name versions already exists.""" # Get all subsets of the current asset - project_name = legacy_io.active_project() + project_name = self.project_name asset_doc = get_asset_by_name( project_name, self.data["asset"], fields=["_id"] ) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index 1a8537adcd..cc45a6c2a8 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -4,7 +4,6 @@ from openpype.hosts.houdini.api import plugin from openpype.hosts.houdini.api.lib import read, imprint from openpype.hosts.houdini.api.pipeline import CONTEXT_CONTAINER from openpype.pipeline import CreatedInstance, AutoCreator -from openpype.pipeline import legacy_io from openpype.client import get_asset_by_name import hou @@ -27,9 +26,9 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): ), None) project_name = self.project_name - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - host_name = legacy_io.Session["AVALON_APP"] + asset_name = self.create_context.get_current_asset_name() + task_name = self.create_context.get_current_task_name() + host_name = self.host_name if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py b/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py index 81274c670e..14a8e3c056 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py +++ b/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py @@ -1,7 +1,6 @@ import pyblish.api from openpype.client import get_subset_by_name, get_asset_by_name -from openpype.pipeline import legacy_io import openpype.lib.usdlib as usdlib @@ -51,7 +50,7 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): self.log.debug("Add bootstrap for: %s" % bootstrap) - project_name = legacy_io.active_project() + project_name = instance.context.data["projectName"] asset = get_asset_by_name(project_name, instance.data["asset"]) assert asset, "Asset must exist: %s" % asset diff --git a/openpype/hosts/houdini/plugins/publish/extract_usd_layered.py b/openpype/hosts/houdini/plugins/publish/extract_usd_layered.py index 8422a3bc3e..d6193f13c1 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_usd_layered.py +++ b/openpype/hosts/houdini/plugins/publish/extract_usd_layered.py @@ -14,7 +14,6 @@ from openpype.client import ( ) from openpype.pipeline import ( get_representation_path, - legacy_io, publish, ) import openpype.hosts.houdini.api.usd as hou_usdlib @@ -250,7 +249,7 @@ class ExtractUSDLayered(publish.Extractor): # Set up the dependency for publish if they have new content # compared to previous publishes - project_name = legacy_io.active_project() + project_name = instance.context.data["projectName"] for dependency in active_dependencies: dependency_fname = dependency.data["usdFilename"] diff --git a/openpype/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py b/openpype/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py index c4f118ac3b..0db782d545 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py +++ b/openpype/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py @@ -4,7 +4,6 @@ import re import pyblish.api from openpype.client import get_subset_by_name -from openpype.pipeline import legacy_io from openpype.pipeline.publish import ValidateContentsOrder from openpype.pipeline import PublishValidationError @@ -18,7 +17,7 @@ class ValidateUSDShadeModelExists(pyblish.api.InstancePlugin): label = "USD Shade model exists" def process(self, instance): - project_name = legacy_io.active_project() + project_name = instance.context.data["projectName"] asset_name = instance.data["asset"] subset = instance.data["subset"] diff --git a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py index 48019e0a82..310d057a11 100644 --- a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py +++ b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py @@ -5,7 +5,7 @@ import husdoutputprocessors.base as base import colorbleed.usdlib as usdlib from openpype.client import get_asset_by_name -from openpype.pipeline import legacy_io, Anatomy +from openpype.pipeline import Anatomy, get_current_project_name class AvalonURIOutputProcessor(base.OutputProcessorBase): @@ -122,7 +122,7 @@ class AvalonURIOutputProcessor(base.OutputProcessorBase): """ - PROJECT = legacy_io.Session["AVALON_PROJECT"] + PROJECT = get_current_project_name() anatomy = Anatomy(PROJECT) asset_doc = get_asset_by_name(PROJECT, asset) if not asset_doc: diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 3074f8e170..90608737c2 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -7,15 +7,18 @@ import os from pymxs import runtime as rt from openpype.hosts.max.api.lib import get_current_renderer -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_project_name from openpype.settings import get_project_settings class RenderProducts(object): def __init__(self, project_settings=None): - self._project_settings = project_settings or get_project_settings( - legacy_io.Session["AVALON_PROJECT"]) + self._project_settings = project_settings + if not self._project_settings: + self._project_settings = get_project_settings( + get_current_project_name() + ) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 91e4a5bf9b..1b62edabee 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -2,7 +2,7 @@ import os from pymxs import runtime as rt from openpype.lib import Logger from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_project_name from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts.max.api.lib import ( @@ -31,7 +31,7 @@ class RenderSettings(object): self._project_settings = project_settings if not self._project_settings: self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] + get_current_project_name() ) def set_render_camera(self, selection): diff --git a/openpype/hosts/max/plugins/publish/collect_workfile.py b/openpype/hosts/max/plugins/publish/collect_workfile.py index 3500b2735c..0eb4bb731e 100644 --- a/openpype/hosts/max/plugins/publish/collect_workfile.py +++ b/openpype/hosts/max/plugins/publish/collect_workfile.py @@ -4,7 +4,6 @@ import os import pyblish.api from pymxs import runtime as rt -from openpype.pipeline import legacy_io class CollectWorkfile(pyblish.api.ContextPlugin): @@ -26,7 +25,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): filename, ext = os.path.splitext(file) - task = legacy_io.Session["AVALON_TASK"] + task = context.data["task"] data = {} @@ -36,7 +35,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): data.update({ "subset": subset, - "asset": os.getenv("AVALON_ASSET", None), + "asset": context.data["asset"], "label": subset, "publish": True, "family": 'workfile', diff --git a/openpype/hosts/max/plugins/publish/validate_pointcloud.py b/openpype/hosts/max/plugins/publish/validate_pointcloud.py index 1ff6eb126f..295a23f1f6 100644 --- a/openpype/hosts/max/plugins/publish/validate_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/validate_pointcloud.py @@ -1,15 +1,6 @@ import pyblish.api from openpype.pipeline import PublishValidationError from pymxs import runtime as rt -from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io - - -def get_setting(project_setting=None): - project_setting = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) - return project_setting["max"]["PointCloud"] class ValidatePointCloud(pyblish.api.InstancePlugin): @@ -108,6 +99,9 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): f"Validating tyFlow custom attributes for {container}") selection_list = instance.data["members"] + + project_setting = instance.data["project_setting"] + attr_settings = project_setting["max"]["PointCloud"]["attribute"] for sel in selection_list: obj = sel.baseobject anim_names = rt.GetSubAnimNames(obj) @@ -118,8 +112,7 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): event_name = sub_anim.name opt = "${0}.{1}.export_particles".format(sel.name, event_name) - attributes = get_setting()["attribute"] - for key, value in attributes.items(): + for key, value in attr_settings.items(): custom_attr = "{0}.PRTChannels_{1}".format(opt, value) try: diff --git a/openpype/hosts/maya/api/action.py b/openpype/hosts/maya/api/action.py index 3b8e2c1848..277f4cc238 100644 --- a/openpype/hosts/maya/api/action.py +++ b/openpype/hosts/maya/api/action.py @@ -4,7 +4,6 @@ from __future__ import absolute_import import pyblish.api from openpype.client import get_asset_by_name -from openpype.pipeline import legacy_io from openpype.pipeline.publish import get_errored_instances_from_context @@ -80,7 +79,7 @@ class GenerateUUIDsOnInvalidAction(pyblish.api.Action): asset_doc = instance.data.get("assetEntity") if not asset_doc: asset_name = instance.data["asset"] - project_name = legacy_io.active_project() + project_name = instance.context.data["projectName"] self.log.info(( "Asset is not stored on instance." " Querying by name \"{}\" from project \"{}\"" diff --git a/openpype/hosts/maya/api/commands.py b/openpype/hosts/maya/api/commands.py index 3e31875fd8..46494413b7 100644 --- a/openpype/hosts/maya/api/commands.py +++ b/openpype/hosts/maya/api/commands.py @@ -3,7 +3,7 @@ from maya import cmds from openpype.client import get_asset_by_name, get_project -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_project_name, get_current_asset_name class ToolWindows: @@ -85,8 +85,8 @@ def reset_resolution(): resolution_height = 1080 # Get resolution from asset - project_name = legacy_io.active_project() - asset_name = legacy_io.Session["AVALON_ASSET"] + project_name = get_current_project_name() + asset_name = get_current_asset_name() asset_doc = get_asset_by_name(project_name, asset_name) resolution = _resolution_from_document(asset_doc) # Try get resolution from project diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index ef8ddf8bac..cdc722a409 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -25,7 +25,8 @@ from openpype.client import ( ) from openpype.settings import get_project_settings from openpype.pipeline import ( - legacy_io, + get_current_project_name, + get_current_asset_name, discover_loader_plugins, loaders_from_representation, get_representation_path, @@ -1413,8 +1414,8 @@ def generate_ids(nodes, asset_id=None): if asset_id is None: # Get the asset ID from the database for the asset of current context - project_name = legacy_io.active_project() - asset_name = legacy_io.Session["AVALON_ASSET"] + project_name = get_current_project_name() + asset_name = get_current_asset_name() asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) assert asset_doc, "No current asset found in Session" asset_id = asset_doc['_id'] @@ -1614,17 +1615,15 @@ def get_container_members(container): # region LOOKDEV -def list_looks(asset_id): +def list_looks(project_name, asset_id): """Return all look subsets for the given asset This assumes all look subsets start with "look*" in their names. """ - # # get all subsets with look leading in # the name associated with the asset # TODO this should probably look for family 'look' instead of checking # subset name that can not start with family - project_name = legacy_io.active_project() subset_docs = get_subsets(project_name, asset_ids=[asset_id]) return [ subset_doc @@ -1646,7 +1645,7 @@ def assign_look_by_version(nodes, version_id): None """ - project_name = legacy_io.active_project() + project_name = get_current_project_name() # Get representations of shader file and relationships look_representation = get_representation_by_name( @@ -1712,7 +1711,7 @@ def assign_look(nodes, subset="lookDefault"): parts = pype_id.split(":", 1) grouped[parts[0]].append(node) - project_name = legacy_io.active_project() + project_name = get_current_project_name() subset_docs = get_subsets( project_name, subset_names=[subset], asset_ids=grouped.keys() ) @@ -2226,6 +2225,35 @@ def set_scene_resolution(width, height, pixelAspect): cmds.setAttr("%s.pixelAspect" % control_node, pixelAspect) +def get_fps_for_current_context(): + """Get fps that should be set for current context. + + Todos: + - Skip project value. + - Merge logic with 'get_frame_range' and 'reset_scene_resolution' -> + all the values in the functions can be collected at one place as + they have same requirements. + + Returns: + Union[int, float]: FPS value. + """ + + project_name = get_current_project_name() + asset_name = get_current_asset_name() + asset_doc = get_asset_by_name( + project_name, asset_name, fields=["data.fps"] + ) or {} + fps = asset_doc.get("data", {}).get("fps") + if not fps: + project_doc = get_project(project_name, fields=["data.fps"]) or {} + fps = project_doc.get("data", {}).get("fps") + + if not fps: + fps = 25 + + return convert_to_maya_fps(fps) + + def get_frame_range(include_animation_range=False): """Get the current assets frame range and handles. @@ -2300,10 +2328,7 @@ def reset_frame_range(playback=True, render=True, fps=True): fps (bool, Optional): Whether to set scene FPS. Defaults to True. """ if fps: - fps = convert_to_maya_fps( - float(legacy_io.Session.get("AVALON_FPS", 25)) - ) - set_scene_fps(fps) + set_scene_fps(get_fps_for_current_context()) frame_range = get_frame_range(include_animation_range=True) if not frame_range: @@ -2339,7 +2364,7 @@ def reset_scene_resolution(): None """ - project_name = legacy_io.active_project() + project_name = get_current_project_name() project_doc = get_project(project_name) project_data = project_doc["data"] asset_data = get_current_project_asset()["data"] @@ -2372,19 +2397,9 @@ def set_context_settings(): None """ - # Todo (Wijnand): apply renderer and resolution of project - project_name = legacy_io.active_project() - project_doc = get_project(project_name) - project_data = project_doc["data"] - asset_doc = get_current_project_asset(fields=["data.fps"]) - asset_data = asset_doc.get("data", {}) # Set project fps - fps = convert_to_maya_fps( - asset_data.get("fps", project_data.get("fps", 25)) - ) - legacy_io.Session["AVALON_FPS"] = str(fps) - set_scene_fps(fps) + set_scene_fps(get_fps_for_current_context()) reset_scene_resolution() @@ -2404,9 +2419,7 @@ def validate_fps(): """ - expected_fps = convert_to_maya_fps( - get_current_project_asset(fields=["data.fps"])["data"]["fps"] - ) + expected_fps = get_fps_for_current_context() current_fps = mel.eval('currentTimeUnitToFPS()') fps_match = current_fps == expected_fps diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index eaa728a2f6..f54633c04d 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -6,13 +6,9 @@ import six import sys from openpype.lib import Logger -from openpype.settings import ( - get_project_settings, - get_current_project_settings -) +from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io -from openpype.pipeline import CreatorError +from openpype.pipeline import CreatorError, get_current_project_name from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts.maya.api.lib import reset_frame_range @@ -27,21 +23,6 @@ class RenderSettings(object): 'mayahardware2': 'defaultRenderGlobals.imageFilePrefix' } - _image_prefixes = { - 'vray': get_current_project_settings()["maya"]["RenderSettings"]["vray_renderer"]["image_prefix"], # noqa - 'arnold': get_current_project_settings()["maya"]["RenderSettings"]["arnold_renderer"]["image_prefix"], # noqa - 'renderman': get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["image_prefix"], # noqa - 'redshift': get_current_project_settings()["maya"]["RenderSettings"]["redshift_renderer"]["image_prefix"] # noqa - } - - # Renderman only - _image_dir = { - 'renderman': get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["image_dir"], # noqa - 'cryptomatte': get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["cryptomatte_dir"], # noqa - 'imageDisplay': get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["imageDisplay_dir"], # noqa - "watermark": get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["watermark_dir"] # noqa - } - _aov_chars = { "dot": ".", "dash": "-", @@ -55,11 +36,30 @@ class RenderSettings(object): return cls._image_prefix_nodes[renderer] def __init__(self, project_settings=None): - self._project_settings = project_settings - if not self._project_settings: - self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] + if not project_settings: + project_settings = get_project_settings( + get_current_project_name() ) + render_settings = project_settings["maya"]["RenderSettings"] + image_prefixes = { + "vray": render_settings["vray_renderer"]["image_prefix"], + "arnold": render_settings["arnold_renderer"]["image_prefix"], + "renderman": render_settings["renderman_renderer"]["image_prefix"], + "redshift": render_settings["redshift_renderer"]["image_prefix"] + } + + # TODO probably should be stored to more explicit attribute + # Renderman only + renderman_settings = render_settings["renderman_renderer"] + _image_dir = { + "renderman": renderman_settings["image_dir"], + "cryptomatte": renderman_settings["cryptomatte_dir"], + "imageDisplay": renderman_settings["imageDisplay_dir"], + "watermark": renderman_settings["watermark_dir"] + } + self._image_prefixes = image_prefixes + self._image_dir = _image_dir + self._project_settings = project_settings def set_default_renderer_settings(self, renderer=None): """Set basic settings based on renderer.""" diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 645d6f5a1c..715f54686c 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -7,7 +7,11 @@ import maya.utils import maya.cmds as cmds from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io +from openpype.pipeline import ( + get_current_project_name, + get_current_asset_name, + get_current_task_name +) from openpype.pipeline.workfile import BuildWorkfile from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib, lib_rendersettings @@ -35,6 +39,13 @@ def _get_menu(menu_name=None): return widgets.get(menu_name) +def get_context_label(): + return "{}, {}".format( + get_current_asset_name(), + get_current_task_name() + ) + + def install(): if cmds.about(batch=True): log.info("Skipping openpype.menu initialization in batch mode..") @@ -45,19 +56,15 @@ def install(): parent_widget = get_main_window() cmds.menu( MENU_NAME, - label=legacy_io.Session["AVALON_LABEL"], + label=os.environ.get("AVALON_LABEL") or "OpenPype", tearOff=True, parent="MayaWindow" ) # Create context menu - context_label = "{}, {}".format( - legacy_io.Session["AVALON_ASSET"], - legacy_io.Session["AVALON_TASK"] - ) cmds.menuItem( "currentContext", - label=context_label, + label=get_context_label(), parent=MENU_NAME, enable=False ) @@ -195,7 +202,8 @@ def install(): return # load configuration of custom menu - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + project_name = get_current_project_name() + project_settings = get_project_settings(project_name) config = project_settings["maya"]["scriptsmenu"]["definition"] _menu = project_settings["maya"]["scriptsmenu"]["name"] @@ -252,8 +260,5 @@ def update_menu_task_label(): log.warning("Can't find menuItem: {}".format(object_name)) return - label = "{}, {}".format( - legacy_io.Session["AVALON_ASSET"], - legacy_io.Session["AVALON_TASK"] - ) + label = get_context_label() cmds.menuItem(object_name, edit=True, label=label) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 2f2ab83f79..60495ac652 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -27,6 +27,9 @@ from openpype.lib import ( ) from openpype.pipeline import ( legacy_io, + get_current_project_name, + get_current_asset_name, + get_current_task_name, register_loader_plugin_path, register_inventory_action_path, register_creator_plugin_path, @@ -75,7 +78,7 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): self._op_events = {} def install(self): - project_name = legacy_io.active_project() + project_name = get_current_project_name() project_settings = get_project_settings(project_name) # process path mapping dirmap_processor = MayaDirmap("maya", project_name, project_settings) @@ -320,7 +323,7 @@ def _remove_workfile_lock(): def handle_workfile_locks(): if lib.IS_HEADLESS: return False - project_name = legacy_io.active_project() + project_name = get_current_project_name() return is_workfile_lock_enabled(MayaHost.name, project_name) @@ -657,9 +660,9 @@ def on_task_changed(): lib.update_content_on_context_change() msg = " project: {}\n asset: {}\n task:{}".format( - legacy_io.active_project(), - legacy_io.Session["AVALON_ASSET"], - legacy_io.Session["AVALON_TASK"] + get_current_project_name(), + get_current_asset_name(), + get_current_task_name() ) lib.show_message( @@ -674,7 +677,7 @@ def before_workfile_open(): def before_workfile_save(event): - project_name = legacy_io.active_project() + project_name = get_current_project_name() if handle_workfile_locks(): _remove_workfile_lock() workdir_path = event["workdir_path"] diff --git a/openpype/hosts/maya/api/setdress.py b/openpype/hosts/maya/api/setdress.py index 0bb1f186eb..7624aacd0f 100644 --- a/openpype/hosts/maya/api/setdress.py +++ b/openpype/hosts/maya/api/setdress.py @@ -18,13 +18,13 @@ from openpype.client import ( ) from openpype.pipeline import ( schema, - legacy_io, discover_loader_plugins, loaders_from_representation, load_container, update_container, remove_container, get_representation_path, + get_current_project_name, ) from openpype.hosts.maya.api.lib import ( matrix_equals, @@ -289,7 +289,7 @@ def update_package_version(container, version): """ # Versioning (from `core.maya.pipeline`) - project_name = legacy_io.active_project() + project_name = get_current_project_name() current_representation = get_representation_by_id( project_name, container["representation"] ) @@ -332,7 +332,7 @@ def update_package(set_container, representation): """ # Load the original package data - project_name = legacy_io.active_project() + project_name = get_current_project_name() current_representation = get_representation_by_id( project_name, set_container["representation"] ) @@ -380,7 +380,7 @@ def update_scene(set_container, containers, current_data, new_data, new_file): """ set_namespace = set_container['namespace'] - project_name = legacy_io.active_project() + project_name = get_current_project_name() # Update the setdress hierarchy alembic set_root = get_container_transforms(set_container, root=True) diff --git a/openpype/hosts/maya/hooks/pre_copy_mel.py b/openpype/hosts/maya/hooks/pre_copy_mel.py index 6f90af4b7c..9cea829ad7 100644 --- a/openpype/hosts/maya/hooks/pre_copy_mel.py +++ b/openpype/hosts/maya/hooks/pre_copy_mel.py @@ -10,10 +10,11 @@ class PreCopyMel(PreLaunchHook): app_groups = ["maya"] def execute(self): - project_name = self.launch_context.env.get("AVALON_PROJECT") + project_doc = self.data["project_doc"] workdir = self.launch_context.env.get("AVALON_WORKDIR") if not workdir: self.log.warning("BUG: Workdir is not filled.") return - create_workspace_mel(workdir, project_name) + project_settings = self.data["project_settings"] + create_workspace_mel(workdir, project_doc["name"], project_settings) diff --git a/openpype/hosts/maya/lib.py b/openpype/hosts/maya/lib.py index ffb2f0b27c..765c60381b 100644 --- a/openpype/hosts/maya/lib.py +++ b/openpype/hosts/maya/lib.py @@ -3,7 +3,7 @@ from openpype.settings import get_project_settings from openpype.lib import Logger -def create_workspace_mel(workdir, project_name): +def create_workspace_mel(workdir, project_name, project_settings=None): dst_filepath = os.path.join(workdir, "workspace.mel") if os.path.exists(dst_filepath): return @@ -11,8 +11,9 @@ def create_workspace_mel(workdir, project_name): if not os.path.exists(workdir): os.makedirs(workdir) - project_setting = get_project_settings(project_name) - mel_script = project_setting["maya"].get("mel_workspace") + if not project_settings: + project_settings = get_project_settings(project_name) + mel_script = project_settings["maya"].get("mel_workspace") # Skip if mel script in settings is empty if not mel_script: diff --git a/openpype/hosts/maya/plugins/inventory/import_modelrender.py b/openpype/hosts/maya/plugins/inventory/import_modelrender.py index 8a7390bc8d..4db8c4f2f6 100644 --- a/openpype/hosts/maya/plugins/inventory/import_modelrender.py +++ b/openpype/hosts/maya/plugins/inventory/import_modelrender.py @@ -8,7 +8,7 @@ from openpype.client import ( from openpype.pipeline import ( InventoryAction, get_representation_context, - legacy_io, + get_current_project_name, ) from openpype.hosts.maya.api.lib import ( maintained_selection, @@ -35,7 +35,7 @@ class ImportModelRender(InventoryAction): def process(self, containers): from maya import cmds - project_name = legacy_io.active_project() + project_name = get_current_project_name() for container in containers: con_name = container["objectName"] nodes = [] @@ -68,7 +68,7 @@ class ImportModelRender(InventoryAction): from maya import cmds - project_name = legacy_io.active_project() + project_name = get_current_project_name() repre_docs = get_representations( project_name, version_ids=[version_id], fields=["_id", "name"] ) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index 9e7fd96bdb..265b15f4ae 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -6,7 +6,7 @@ from openpype.client import ( get_version_by_id, ) from openpype.pipeline import ( - legacy_io, + get_current_project_name, load, get_representation_path, ) @@ -68,7 +68,7 @@ class AudioLoader(load.LoaderPlugin): ) # Set frame range. - project_name = legacy_io.active_project() + project_name = get_current_project_name() version = get_version_by_id( project_name, representation["parent"], fields=["parent"] ) diff --git a/openpype/hosts/maya/plugins/load/load_gpucache.py b/openpype/hosts/maya/plugins/load/load_gpucache.py index 52be1ca645..344f2fd060 100644 --- a/openpype/hosts/maya/plugins/load/load_gpucache.py +++ b/openpype/hosts/maya/plugins/load/load_gpucache.py @@ -37,7 +37,8 @@ class GpuCacheLoader(load.LoaderPlugin): label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) - settings = get_project_settings(os.environ['AVALON_PROJECT']) + project_name = context["project"]["name"] + settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get('model') if c is not None: diff --git a/openpype/hosts/maya/plugins/load/load_image.py b/openpype/hosts/maya/plugins/load/load_image.py index 552bcc33af..3b1f5442ce 100644 --- a/openpype/hosts/maya/plugins/load/load_image.py +++ b/openpype/hosts/maya/plugins/load/load_image.py @@ -4,7 +4,8 @@ import copy from openpype.lib import EnumDef from openpype.pipeline import ( load, - get_representation_context + get_representation_context, + get_current_host_name, ) from openpype.pipeline.load.utils import get_representation_path_from_context from openpype.pipeline.colorspace import ( @@ -266,7 +267,7 @@ class FileNodeLoader(load.LoaderPlugin): # Assume colorspace from filepath based on project settings project_name = context["project"]["name"] - host_name = os.environ.get("AVALON_APP") + host_name = get_current_host_name() project_settings = get_project_settings(project_name) config_data = get_imageio_config( diff --git a/openpype/hosts/maya/plugins/load/load_image_plane.py b/openpype/hosts/maya/plugins/load/load_image_plane.py index bf13708e9b..117f4f4202 100644 --- a/openpype/hosts/maya/plugins/load/load_image_plane.py +++ b/openpype/hosts/maya/plugins/load/load_image_plane.py @@ -6,9 +6,9 @@ from openpype.client import ( get_version_by_id, ) from openpype.pipeline import ( - legacy_io, load, - get_representation_path + get_representation_path, + get_current_project_name, ) from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import ( @@ -221,7 +221,7 @@ class ImagePlaneLoader(load.LoaderPlugin): type="string") # Set frame range. - project_name = legacy_io.active_project() + project_name = get_current_project_name() version = get_version_by_id( project_name, representation["parent"], fields=["parent"] ) diff --git a/openpype/hosts/maya/plugins/load/load_look.py b/openpype/hosts/maya/plugins/load/load_look.py index 3cc87b7ef4..1d96831d89 100644 --- a/openpype/hosts/maya/plugins/load/load_look.py +++ b/openpype/hosts/maya/plugins/load/load_look.py @@ -7,7 +7,7 @@ from qtpy import QtWidgets from openpype.client import get_representation_by_name from openpype.pipeline import ( - legacy_io, + get_current_project_name, get_representation_path, ) import openpype.hosts.maya.api.plugin @@ -78,7 +78,7 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): shader_nodes = cmds.ls(members, type='shadingEngine') nodes = set(self._get_nodes_with_shader(shader_nodes)) - project_name = legacy_io.active_project() + project_name = get_current_project_name() json_representation = get_representation_by_name( project_name, "json", representation["parent"] ) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index a44482d21d..b3fbfb2ed9 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -57,7 +57,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): return # colour the group node - settings = get_project_settings(os.environ['AVALON_PROJECT']) + project_name = context["project"]["name"] + settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 5c838fbc3c..d339aff69c 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -118,6 +118,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): except ValueError: family = "model" + project_name = context["project"]["name"] # True by default to keep legacy behaviours attach_to_root = options.get("attach_to_root", True) group_name = options["group_name"] @@ -125,9 +126,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): path = self.filepath_from_context(context) with maintained_selection(): cmds.loadPlugin("AbcImport.mll", quiet=True) - file_url = self.prepare_root_value(path, - context["project"]["name"]) + file_url = self.prepare_root_value(path, project_name) nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, @@ -163,7 +163,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): with parent_nodes(roots, parent=None): cmds.xform(group_name, zeroTransformPivots=True) - settings = get_project_settings(os.environ['AVALON_PROJECT']) + settings = get_project_settings(project_name) display_handle = settings['maya']['load'].get( 'reference_loader', {} diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py b/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py index 13fd156216..0f674a69c4 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py @@ -48,7 +48,8 @@ class LoadVDBtoArnold(load.LoaderPlugin): label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) - settings = get_project_settings(os.environ['AVALON_PROJECT']) + project_name = context["project"]["name"] + settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py index 464d5b0ba8..28cfdc7129 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py @@ -67,7 +67,8 @@ class LoadVDBtoRedShift(load.LoaderPlugin): label = "{}:{}".format(namespace, name) root = cmds.createNode("transform", name=label) - settings = get_project_settings(os.environ['AVALON_PROJECT']) + project_name = context["project"]["name"] + settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py index 69eb44a5e9..46f2dd674d 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -127,7 +127,8 @@ class LoadVDBtoVRay(load.LoaderPlugin): label = "{}:{}_VDB".format(namespace, name) root = cmds.group(name=label, empty=True) - settings = get_project_settings(os.environ['AVALON_PROJECT']) + project_name = context["project"]["name"] + settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) diff --git a/openpype/hosts/maya/plugins/load/load_vrayproxy.py b/openpype/hosts/maya/plugins/load/load_vrayproxy.py index 77efdb2069..9d926a33ed 100644 --- a/openpype/hosts/maya/plugins/load/load_vrayproxy.py +++ b/openpype/hosts/maya/plugins/load/load_vrayproxy.py @@ -12,9 +12,9 @@ import maya.cmds as cmds from openpype.client import get_representation_by_name from openpype.settings import get_project_settings from openpype.pipeline import ( - legacy_io, load, - get_representation_path + get_current_project_name, + get_representation_path, ) from openpype.hosts.maya.api.lib import ( maintained_selection, @@ -78,7 +78,8 @@ class VRayProxyLoader(load.LoaderPlugin): return # colour the group node - settings = get_project_settings(os.environ['AVALON_PROJECT']) + project_name = context["project"]["name"] + settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: @@ -187,7 +188,7 @@ class VRayProxyLoader(load.LoaderPlugin): """ self.log.debug( "Looking for abc in published representations of this version.") - project_name = legacy_io.active_project() + project_name = get_current_project_name() abc_rep = get_representation_by_name(project_name, "abc", version_id) if abc_rep: self.log.debug("Found, we'll link alembic to vray proxy.") diff --git a/openpype/hosts/maya/plugins/load/load_vrayscene.py b/openpype/hosts/maya/plugins/load/load_vrayscene.py index f9169ff884..3a2c3a47f2 100644 --- a/openpype/hosts/maya/plugins/load/load_vrayscene.py +++ b/openpype/hosts/maya/plugins/load/load_vrayscene.py @@ -56,7 +56,8 @@ class VRaySceneLoader(load.LoaderPlugin): return # colour the group node - settings = get_project_settings(os.environ['AVALON_PROJECT']) + project_name = context["project"]["name"] + settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py index 833be79ef2..5cded13d4e 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py @@ -68,8 +68,9 @@ class YetiCacheLoader(load.LoaderPlugin): group_name = "{}:{}".format(namespace, name) group_node = cmds.group(nodes, name=group_name) + project_name = context["project"]["name"] - settings = get_project_settings(os.environ['AVALON_PROJECT']) + settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) diff --git a/openpype/hosts/maya/plugins/publish/extract_layout.py b/openpype/hosts/maya/plugins/publish/extract_layout.py index 7921fca069..bf5b4fc0e7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_layout.py +++ b/openpype/hosts/maya/plugins/publish/extract_layout.py @@ -6,7 +6,7 @@ from maya import cmds from maya.api import OpenMaya as om from openpype.client import get_representation_by_id -from openpype.pipeline import legacy_io, publish +from openpype.pipeline import publish class ExtractLayout(publish.Extractor): @@ -30,7 +30,7 @@ class ExtractLayout(publish.Extractor): json_data = [] # TODO representation queries can be refactored to be faster - project_name = legacy_io.active_project() + project_name = instance.context.data["projectName"] for asset in cmds.sets(str(instance), query=True): # Find the container diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py index 298c3bd345..8e219eae85 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py @@ -265,6 +265,8 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): context = instance.context workspace = context.data["workspaceDir"] + project_name = context.data["projectName"] + asset_name = context.data["asset"] filepath = None @@ -371,8 +373,8 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): "jobId": -1, "startOn": 0, "parentId": -1, - "project": os.environ.get('AVALON_PROJECT') or scene, - "shot": os.environ.get('AVALON_ASSET') or scene, + "project": project_name or scene, + "shot": asset_name or scene, "camera": instance.data.get("cameras")[0], "dependMode": 0, "packetSize": 4, diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index ae6a999d98..595ea7880d 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,7 +1,7 @@ import os from openpype.settings import get_project_settings -from openpype.pipeline import install_host +from openpype.pipeline import install_host, get_current_project_name from openpype.hosts.maya.api import MayaHost from maya import cmds @@ -12,7 +12,8 @@ install_host(host) print("Starting OpenPype usersetup...") -project_settings = get_project_settings(os.environ['AVALON_PROJECT']) +project_name = get_current_project_name() +settings = get_project_settings(project_name) # Loading plugins explicitly. explicit_plugins_loading = project_settings["maya"]["explicit_plugins_loading"] @@ -46,17 +47,16 @@ if bool(int(os.environ.get(key, "0"))): ) # Build a shelf. -shelf_preset = project_settings['maya'].get('project_shelf') - +shelf_preset = settings['maya'].get('project_shelf') if shelf_preset: - project = os.environ["AVALON_PROJECT"] - - icon_path = os.path.join(os.environ['OPENPYPE_PROJECT_SCRIPTS'], - project, "icons") + icon_path = os.path.join( + os.environ['OPENPYPE_PROJECT_SCRIPTS'], + project_name, + "icons") icon_path = os.path.abspath(icon_path) for i in shelf_preset['imports']: - import_string = "from {} import {}".format(project, i) + import_string = "from {} import {}".format(project_name, i) print(import_string) exec(import_string) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index 64fc04dfc4..b5ce7ada34 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -4,9 +4,9 @@ import logging from qtpy import QtWidgets, QtCore -from openpype.client import get_last_version_by_subset_id from openpype import style -from openpype.pipeline import legacy_io +from openpype.client import get_last_version_by_subset_id +from openpype.pipeline import get_current_project_name from openpype.tools.utils.lib import qt_app_context from openpype.hosts.maya.api.lib import ( assign_look_by_version, @@ -216,7 +216,7 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): selection = self.assign_selected.isChecked() asset_nodes = self.asset_outliner.get_nodes(selection=selection) - project_name = legacy_io.active_project() + project_name = get_current_project_name() start = time.time() for i, (asset, item) in enumerate(asset_nodes.items()): diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index c5e6c973cf..a1290aa68d 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -1,14 +1,14 @@ -from collections import defaultdict -import logging import os +import logging +from collections import defaultdict import maya.cmds as cmds -from openpype.client import get_asset_by_id +from openpype.client import get_assets from openpype.pipeline import ( - legacy_io, remove_container, registered_host, + get_current_project_name, ) from openpype.hosts.maya.api import lib @@ -126,18 +126,24 @@ def create_items_from_nodes(nodes): log.warning("No id hashes") return asset_view_items - project_name = legacy_io.active_project() - for _id, id_nodes in id_hashes.items(): - asset = get_asset_by_id(project_name, _id, fields=["name"]) + project_name = get_current_project_name() + asset_ids = set(id_hashes.keys()) + asset_docs = get_assets(project_name, asset_ids, fields=["name"]) + asset_docs_by_id = { + str(asset_doc["_id"]): asset_doc + for asset_doc in asset_docs + } + for asset_id, id_nodes in id_hashes.items(): + asset_doc = asset_docs_by_id.get(asset_id) # Skip if asset id is not found - if not asset: + if not asset_doc: log.warning("Id not found in the database, skipping '%s'." % _id) log.warning("Nodes: %s" % id_nodes) continue # Collect available look subsets for this asset - looks = lib.list_looks(asset["_id"]) + looks = lib.list_looks(project_name, asset_doc["_id"]) # Collect namespaces the asset is found in namespaces = set() @@ -146,8 +152,8 @@ def create_items_from_nodes(nodes): namespaces.add(namespace) asset_view_items.append({ - "label": asset["name"], - "asset": asset, + "label": asset_doc["name"], + "asset": asset_doc, "looks": looks, "namespaces": namespaces }) diff --git a/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py b/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py index c875fec7f0..97fb832f71 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py +++ b/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py @@ -6,7 +6,7 @@ import logging from maya import cmds from openpype.client import get_last_version_by_subset_name -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_project_name import openpype.hosts.maya.lib as maya_lib from . import lib from .alembic import get_alembic_ids_cache @@ -76,7 +76,7 @@ def vrayproxy_assign_look(vrayproxy, subset="lookDefault"): asset_id = node_id.split(":", 1)[0] node_ids_by_asset_id[asset_id].add(node_id) - project_name = legacy_io.active_project() + project_name = get_current_project_name() for asset_id, node_ids in node_ids_by_asset_id.items(): # Get latest look version diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index f930dec720..364c8eeff4 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -42,8 +42,10 @@ from openpype.pipeline.template_data import get_template_data_with_names from openpype.pipeline import ( get_current_project_name, discover_legacy_creator_plugins, - legacy_io, Anatomy, + get_current_host_name, + get_current_project_name, + get_current_asset_name, ) from openpype.pipeline.context_tools import ( get_current_project_asset, @@ -970,7 +972,7 @@ def check_inventory_versions(): if not repre_ids: return - project_name = legacy_io.active_project() + project_name = get_current_project_name() # Find representations based on found containers repre_docs = get_representations( project_name, @@ -1146,7 +1148,7 @@ def format_anatomy(data): project_name = anatomy.project_name asset_name = data["asset"] task_name = data["task"] - host_name = os.environ["AVALON_APP"] + host_name = get_current_host_name() context_data = get_template_data_with_names( project_name, asset_name, task_name, host_name ) @@ -1474,7 +1476,7 @@ def create_write_node_legacy( if knob["name"] == "file_type": representation = knob["value"] - host_name = os.environ.get("AVALON_APP") + host_name = get_current_host_name() try: data.update({ "app": host_name, @@ -1933,15 +1935,18 @@ class WorkfileSettings(object): def __init__(self, root_node=None, nodes=None, **kwargs): project_doc = kwargs.get("project") if project_doc is None: - project_name = legacy_io.active_project() + project_name = get_current_project_name() project_doc = get_project(project_name) + else: + project_name = project_doc["name"] Context._project_doc = project_doc + self._project_name = project_name self._asset = ( kwargs.get("asset_name") - or legacy_io.Session["AVALON_ASSET"] + or get_current_asset_name() ) - self._asset_entity = get_current_project_asset(self._asset) + self._asset_entity = get_asset_by_name(project_name, self._asset) self._root_node = root_node or nuke.root() self._nodes = self.get_nodes(nodes=nodes) @@ -2334,7 +2339,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. def reset_resolution(self): """Set resolution to project resolution.""" log.info("Resetting resolution") - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset_data = self._asset_entity["data"] format_data = { @@ -2413,7 +2418,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. from .utils import set_context_favorites work_dir = os.getenv("AVALON_WORKDIR") - asset = os.getenv("AVALON_ASSET") + asset = get_current_asset_name() favorite_items = OrderedDict() # project @@ -2836,7 +2841,8 @@ def add_scripts_menu(): return # load configuration of custom menu - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + project_name = get_current_project_name() + project_settings = get_project_settings(project_name) config = project_settings["nuke"]["scriptsmenu"]["definition"] _menu = project_settings["nuke"]["scriptsmenu"]["name"] @@ -2854,7 +2860,8 @@ def add_scripts_menu(): def add_scripts_gizmo(): # load configuration of custom menu - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + project_name = get_current_project_name() + project_settings = get_project_settings(project_name) platform_name = platform.system().lower() for gizmo_settings in project_settings["nuke"]["gizmo"]: diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 8406a251e9..cdfc8aa512 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -20,6 +20,8 @@ from openpype.pipeline import ( register_creator_plugin_path, register_inventory_action_path, AVALON_CONTAINER_ID, + get_current_asset_name, + get_current_task_name, ) from openpype.pipeline.workfile import BuildWorkfile from openpype.tools.utils import host_tools @@ -211,6 +213,13 @@ def _show_workfiles(): host_tools.show_workfiles(parent=None, on_top=False) +def get_context_label(): + return "{0}, {1}".format( + get_current_asset_name(), + get_current_task_name() + ) + + def _install_menu(): """Install Avalon menu into Nuke's main menu bar.""" @@ -220,9 +229,7 @@ def _install_menu(): menu = menubar.addMenu(MENU_LABEL) if not ASSIST: - label = "{0}, {1}".format( - os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] - ) + label = get_context_label() Context.context_label = label context_action = menu.addCommand(label) context_action.setEnabled(False) @@ -338,9 +345,7 @@ def change_context_label(): menubar = nuke.menu("Nuke") menu = menubar.findItem(MENU_LABEL) - label = "{0}, {1}".format( - os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] - ) + label = get_context_label() rm_item = [ (i, item) for i, item in enumerate(menu.items()) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 7035da2bb5..f7bbea3d01 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -19,7 +19,7 @@ from openpype.pipeline import ( CreatorError, Creator as NewCreator, CreatedInstance, - legacy_io + get_current_task_name ) from .lib import ( INSTANCE_DATA_KNOB, @@ -1173,7 +1173,7 @@ def convert_to_valid_instaces(): from openpype.hosts.nuke.api import workio - task_name = legacy_io.Session["AVALON_TASK"] + task_name = get_current_task_name() # save into new workfile current_file = workio.current_file() diff --git a/openpype/hosts/nuke/plugins/create/workfile_creator.py b/openpype/hosts/nuke/plugins/create/workfile_creator.py index 72ef61e63f..c4e0753abc 100644 --- a/openpype/hosts/nuke/plugins/create/workfile_creator.py +++ b/openpype/hosts/nuke/plugins/create/workfile_creator.py @@ -3,7 +3,6 @@ from openpype.client import get_asset_by_name from openpype.pipeline import ( AutoCreator, CreatedInstance, - legacy_io, ) from openpype.hosts.nuke.api import ( INSTANCE_DATA_KNOB, @@ -27,10 +26,10 @@ class WorkfileCreator(AutoCreator): root_node, api.INSTANCE_DATA_KNOB ) - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - host_name = legacy_io.Session["AVALON_APP"] + project_name = self.create_context.get_current_project_name() + asset_name = self.create_context.get_current_asset_name() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index f4f581e6a4..fe82d70b5e 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -6,8 +6,8 @@ from openpype.client import ( get_last_version_by_subset_id, ) from openpype.pipeline import ( - legacy_io, load, + get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import ( @@ -190,7 +190,7 @@ class LoadBackdropNodes(load.LoaderPlugin): # get main variables # Get version from io - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index 951457475d..fec4ee556e 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -5,8 +5,8 @@ from openpype.client import ( get_last_version_by_subset_id ) from openpype.pipeline import ( - legacy_io, load, + get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api import ( @@ -108,7 +108,7 @@ class AlembicCameraLoader(load.LoaderPlugin): None """ # Get version from io - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) object_name = container['objectName'] @@ -180,7 +180,7 @@ class AlembicCameraLoader(load.LoaderPlugin): """ Coloring a node by correct color by actual version """ # get all versions in list - project_name = legacy_io.active_project() + project_name = get_current_project_name() last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 92f01e560a..5539324fb7 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -8,7 +8,7 @@ from openpype.client import ( get_last_version_by_subset_id, ) from openpype.pipeline import ( - legacy_io, + get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import ( @@ -270,7 +270,7 @@ class LoadClip(plugin.NukeLoader): if "addRetime" in key ] - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) version_data = version_doc.get("data", {}) diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index 6e56fe4a56..89597e76cc 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -8,8 +8,8 @@ from openpype.client import ( get_last_version_by_subset_id, ) from openpype.pipeline import ( - legacy_io, load, + get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api import ( @@ -155,7 +155,7 @@ class LoadEffects(load.LoaderPlugin): """ # get main variables # Get version from io - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index 95452919e2..efe67be4aa 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -8,8 +8,8 @@ from openpype.client import ( get_last_version_by_subset_id, ) from openpype.pipeline import ( - legacy_io, load, + get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api import lib @@ -160,7 +160,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): # get main variables # Get version from io - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo.py b/openpype/hosts/nuke/plugins/load/load_gizmo.py index c7a40e0d00..6b848ee276 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo.py @@ -5,8 +5,8 @@ from openpype.client import ( get_last_version_by_subset_id, ) from openpype.pipeline import ( - legacy_io, load, + get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import ( @@ -106,7 +106,7 @@ class LoadGizmo(load.LoaderPlugin): # get main variables # Get version from io - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index ceb8ee9609..a8e1218cbe 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -6,8 +6,8 @@ from openpype.client import ( get_last_version_by_subset_id, ) from openpype.pipeline import ( - legacy_io, load, + get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import ( @@ -113,7 +113,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): # get main variables # Get version from io - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 0ff39482d9..d8c0a82206 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -7,8 +7,8 @@ from openpype.client import ( get_last_version_by_subset_id, ) from openpype.pipeline import ( - legacy_io, load, + get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import ( @@ -201,7 +201,7 @@ class LoadImage(load.LoaderPlugin): format(frame_number, "0{}".format(padding))) # Get start frame from version data - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index fad29d0d0b..0bdcd93dff 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -5,8 +5,8 @@ from openpype.client import ( get_last_version_by_subset_id, ) from openpype.pipeline import ( - legacy_io, load, + get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import maintained_selection @@ -112,7 +112,7 @@ class AlembicModelLoader(load.LoaderPlugin): None """ # Get version from io - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) object_name = container['objectName'] # get corresponding node @@ -187,7 +187,7 @@ class AlembicModelLoader(load.LoaderPlugin): def node_version_color(self, version, node): """ Coloring a node by correct color by actual version""" - project_name = legacy_io.active_project() + project_name = get_current_project_name() last_version_doc = get_last_version_by_subset_id( project_name, version["parent"], fields=["_id"] ) diff --git a/openpype/hosts/nuke/plugins/load/load_script_precomp.py b/openpype/hosts/nuke/plugins/load/load_script_precomp.py index 7fd7c13c48..48d4a0900a 100644 --- a/openpype/hosts/nuke/plugins/load/load_script_precomp.py +++ b/openpype/hosts/nuke/plugins/load/load_script_precomp.py @@ -5,7 +5,7 @@ from openpype.client import ( get_last_version_by_subset_id, ) from openpype.pipeline import ( - legacy_io, + get_current_project_name, load, get_representation_path, ) @@ -123,7 +123,7 @@ class LinkAsGroup(load.LoaderPlugin): root = get_representation_path(representation).replace("\\", "/") # Get start frame from version data - project_name = legacy_io.active_project() + project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] diff --git a/openpype/hosts/nuke/plugins/publish/collect_reads.py b/openpype/hosts/nuke/plugins/publish/collect_reads.py index 831ae29a27..38938a3dda 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_reads.py +++ b/openpype/hosts/nuke/plugins/publish/collect_reads.py @@ -2,8 +2,6 @@ import os import re import nuke import pyblish.api -from openpype.client import get_asset_by_name -from openpype.pipeline import legacy_io class CollectNukeReads(pyblish.api.InstancePlugin): @@ -15,16 +13,9 @@ class CollectNukeReads(pyblish.api.InstancePlugin): families = ["source"] def process(self, instance): - node = instance.data["transientData"]["node"] - - project_name = legacy_io.active_project() - asset_name = legacy_io.Session["AVALON_ASSET"] - asset_doc = get_asset_by_name(project_name, asset_name) - - self.log.debug("asset_doc: {}".format(asset_doc["data"])) - self.log.debug("checking instance: {}".format(instance)) + node = instance.data["transientData"]["node"] if node.Class() != "Read": return diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 73dc80260c..88f5d63a72 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -6,11 +6,8 @@ import pyblish.api from openpype.lib import register_event_callback, Logger from openpype.pipeline import ( - legacy_io, register_loader_plugin_path, register_creator_plugin_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) @@ -111,14 +108,6 @@ class PhotoshopHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): item["id"] = "publish_context" _get_stub().imprint(item["id"], item) - def get_context_title(self): - """Returns title for Creator window""" - - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - return "{}/{}/{}".format(project_name, asset_name, task_name) - def list_instances(self): """List all created instances to publish from current workfile. diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py index b9d721dbdb..1a4932fe99 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py @@ -1,6 +1,6 @@ import pyblish.api -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_asset_name from openpype.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, @@ -28,10 +28,10 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) stub = photoshop.stub() + current_asset_name = get_current_asset_name() for instance in instances: data = stub.read(instance[0]) - - data["asset"] = legacy_io.Session["AVALON_ASSET"] + data["asset"] = current_asset_name stub.imprint(instance[0], data) @@ -55,7 +55,7 @@ class ValidateInstanceAsset(OptionalPyblishPluginMixin, def process(self, instance): instance_asset = instance.data["asset"] - current_asset = legacy_io.Session["AVALON_ASSET"] + current_asset = get_current_asset_name() if instance_asset != current_asset: msg = ( diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 3518597e8c..3a59ecea80 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -7,7 +7,7 @@ from openpype.client import ( # from openpype.hosts import resolve from openpype.pipeline import ( get_representation_path, - legacy_io, + get_current_project_name, ) from openpype.hosts.resolve.api import lib, plugin from openpype.hosts.resolve.api.pipeline import ( @@ -110,7 +110,7 @@ class LoadClip(plugin.TimelineItemLoader): namespace = container['namespace'] timeline_item_data = lib.get_pype_timeline_item_by_name(namespace) timeline_item = timeline_item_data["clip"]["item"] - project_name = legacy_io.active_project() + project_name = get_current_project_name() version = get_version_by_id(project_name, representation["parent"]) version_data = version.get("data", {}) version_name = version.get("name", None) @@ -153,7 +153,7 @@ class LoadClip(plugin.TimelineItemLoader): # define version name version_name = version.get("name", None) # get all versions in list - project_name = legacy_io.active_project() + project_name = get_current_project_name() last_version_doc = get_last_version_by_subset_id( project_name, version["parent"], diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index 0f94216556..a2f3eaed7a 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -1,8 +1,8 @@ import pyblish.api from pprint import pformat +from openpype.pipeline import get_current_asset_name from openpype.hosts.resolve import api as rapi -from openpype.pipeline import legacy_io from openpype.hosts.resolve.otio import davinci_export @@ -14,7 +14,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): def process(self, context): - asset = legacy_io.Session["AVALON_ASSET"] + asset = get_current_asset_name() subset = "workfile" project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py index a7ae02a2eb..fd2d4a9f36 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py @@ -1,7 +1,5 @@ -import os import pyblish.api -from openpype.settings import get_project_settings from openpype.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, @@ -21,27 +19,30 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): optional = True def process(self, instance): - if instance.data["family"] == "workfile": - ext = instance.data["representations"][0]["ext"] - main_workfile_extensions = self.get_main_workfile_extensions() - if ext not in main_workfile_extensions: - self.log.warning("Only secondary workfile present!") - return + if instance.data["family"] != "workfile": + return - if not instance.data.get("resources"): - msg = "No secondary workfile present for workfile '{}'". \ - format(instance.data["name"]) - ext = main_workfile_extensions[0] - formatting_data = {"file_name": instance.data["name"], - "extension": ext} + ext = instance.data["representations"][0]["ext"] + main_workfile_extensions = self.get_main_workfile_extensions( + instance + ) + if ext not in main_workfile_extensions: + self.log.warning("Only secondary workfile present!") + return - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data - ) + if not instance.data.get("resources"): + msg = "No secondary workfile present for workfile '{}'". \ + format(instance.data["name"]) + ext = main_workfile_extensions[0] + formatting_data = {"file_name": instance.data["name"], + "extension": ext} + + raise PublishXmlValidationError( + self, msg, formatting_data=formatting_data) @staticmethod - def get_main_workfile_extensions(): - project_settings = get_project_settings(os.environ["AVALON_PROJECT"]) + def get_main_workfile_extensions(instance): + project_settings = instance.context.data["project_settings"] try: extensions = (project_settings["standalonepublisher"] diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py index 9492d3d5eb..2155a1bbd5 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_workfile.py +++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py @@ -3,7 +3,7 @@ import os from openpype.lib import StringTemplate from openpype.pipeline import ( registered_host, - legacy_io, + get_current_context, Anatomy, ) from openpype.pipeline.workfile import ( @@ -55,9 +55,10 @@ class LoadWorkfile(plugin.Loader): task_name = work_context.get("task") # Far cases when there is workfile without work_context if not asset_name: - project_name = legacy_io.active_project() - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] + context = get_current_context() + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] template_key = get_workfile_template_key_from_context( asset_name, diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 3d98b3d8e1..d663ce20ea 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -12,7 +12,7 @@ from unreal import ( from openpype.client import get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, - legacy_io, + get_current_project_name, ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( @@ -184,7 +184,7 @@ class CameraLoader(plugin.Loader): frame_ranges[i + 1][0], frame_ranges[i + 1][1], [level]) - project_name = legacy_io.active_project() + project_name = get_current_project_name() data = get_asset_by_name(project_name, asset)["data"] cam_seq.set_display_rate( unreal.FrameRate(data.get("fps"), 1.0)) @@ -390,7 +390,7 @@ class CameraLoader(plugin.Loader): # Set range of all sections # Changing the range of the section is not enough. We need to change # the frame of all the keys in the section. - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset = container.get('asset') data = get_asset_by_name(project_name, asset)["data"] diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e9f3c79960..3b82da5068 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -23,7 +23,7 @@ from openpype.pipeline import ( load_container, get_representation_path, AYON_CONTAINER_ID, - legacy_io, + get_current_project_name, ) from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_current_project_settings @@ -302,7 +302,7 @@ class LayoutLoader(plugin.Loader): if not version_ids: return output - project_name = legacy_io.active_project() + project_name = get_current_project_name() repre_docs = get_representations( project_name, representation_names=["fbx", "abc"], @@ -603,7 +603,7 @@ class LayoutLoader(plugin.Loader): frame_ranges[i + 1][0], frame_ranges[i + 1][1], [level]) - project_name = legacy_io.active_project() + project_name = get_current_project_name() data = get_asset_by_name(project_name, asset)["data"] shot.set_display_rate( unreal.FrameRate(data.get("fps"), 1.0)) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 32fff84152..c53e92930a 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -11,7 +11,7 @@ from openpype.pipeline import ( load_container, get_representation_path, AYON_CONTAINER_ID, - legacy_io, + get_current_project_name, ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as upipeline @@ -411,7 +411,7 @@ class ExistingLayoutLoader(plugin.Loader): asset_dir = container.get('namespace') source_path = get_representation_path(representation) - project_name = legacy_io.active_project() + project_name = get_current_project_name() containers = self._process(source_path, project_name) data = { diff --git a/openpype/hosts/unreal/plugins/publish/extract_layout.py b/openpype/hosts/unreal/plugins/publish/extract_layout.py index 57e7957575..d30d04551d 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_layout.py +++ b/openpype/hosts/unreal/plugins/publish/extract_layout.py @@ -8,7 +8,7 @@ from unreal import EditorLevelLibrary as ell from unreal import EditorAssetLibrary as eal from openpype.client import get_representation_by_name -from openpype.pipeline import legacy_io, publish +from openpype.pipeline import publish class ExtractLayout(publish.Extractor): @@ -32,7 +32,7 @@ class ExtractLayout(publish.Extractor): "Wrong level loaded" json_data = [] - project_name = legacy_io.active_project() + project_name = instance.context.data["projectName"] for member in instance[:]: actor = ell.get_actor_reference(member) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 8adae34827..f47e11926c 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1371,14 +1371,9 @@ def get_app_environments_for_context( """ from openpype.modules import ModulesManager - from openpype.pipeline import AvalonMongoDB, Anatomy + from openpype.pipeline import Anatomy from openpype.lib.openpype_version import is_running_staging - # Avalon database connection - dbcon = AvalonMongoDB() - dbcon.Session["AVALON_PROJECT"] = project_name - dbcon.install() - # Project document project_doc = get_project(project_name) asset_doc = get_asset_by_name(project_name, asset_name) @@ -1400,7 +1395,6 @@ def get_app_environments_for_context( "app": app, - "dbcon": dbcon, "project_doc": project_doc, "asset_doc": asset_doc, @@ -1415,9 +1409,6 @@ def get_app_environments_for_context( prepare_app_environments(data, env_group, modules_manager) prepare_context_environments(data, env_group, modules_manager) - # Discard avalon connection - dbcon.uninstall() - return data["env"] diff --git a/openpype/lib/usdlib.py b/openpype/lib/usdlib.py index 5ef1d38f87..cb96a0c1d0 100644 --- a/openpype/lib/usdlib.py +++ b/openpype/lib/usdlib.py @@ -9,7 +9,7 @@ except ImportError: from mvpxr import Usd, UsdGeom, Sdf, Kind from openpype.client import get_project, get_asset_by_name -from openpype.pipeline import legacy_io, Anatomy +from openpype.pipeline import Anatomy, get_current_project_name log = logging.getLogger(__name__) @@ -126,7 +126,7 @@ def create_model(filename, asset, variant_subsets): """ - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset_doc = get_asset_by_name(project_name, asset) assert asset_doc, "Asset not found: %s" % asset @@ -177,7 +177,7 @@ def create_shade(filename, asset, variant_subsets): """ - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset_doc = get_asset_by_name(project_name, asset) assert asset_doc, "Asset not found: %s" % asset @@ -213,7 +213,7 @@ def create_shade_variation(filename, asset, model_variant, shade_variants): """ - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset_doc = get_asset_by_name(project_name, asset) assert asset_doc, "Asset not found: %s" % asset @@ -314,7 +314,7 @@ def get_usd_master_path(asset, subset, representation): """ - project_name = legacy_io.active_project() + project_name = get_current_project_name() anatomy = Anatomy(project_name) project_doc = get_project( project_name, diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index b6a30e36b7..de3c7221d3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -179,20 +179,18 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, } self.log.debug("Submitting 3dsMax render..") - payload = self._use_published_name(payload_data) + project_settings = instance.context.data["project_settings"] + payload = self._use_published_name(payload_data, project_settings) job_info, plugin_info = payload self.submit(self.assemble_payload(job_info, plugin_info)) - def _use_published_name(self, data): + def _use_published_name(self, data, project_settings): instance = self._instance job_info = copy.deepcopy(self.job_info) plugin_info = copy.deepcopy(self.plugin_info) plugin_data = {} - project_setting = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) - multipass = get_multipass_setting(project_setting) + multipass = get_multipass_setting(project_settings) if multipass: plugin_data["DisableMultipass"] = 0 else: diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 3b04f6d3bc..39120f7c8a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -110,8 +110,8 @@ class MayaSubmitRemotePublishDeadline( # TODO replace legacy_io with context.data environment["AVALON_PROJECT"] = project_name - environment["AVALON_ASSET"] = legacy_io.Session["AVALON_ASSET"] - environment["AVALON_TASK"] = legacy_io.Session["AVALON_TASK"] + environment["AVALON_ASSET"] = instance.context.data["asset"] + environment["AVALON_TASK"] = instance.context.data["task"] environment["AVALON_APP_NAME"] = os.environ.get("AVALON_APP_NAME") environment["OPENPYPE_LOG_NO_COLORS"] = "1" environment["OPENPYPE_REMOTE_JOB"] = "1" diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c893f43c4c..01a5c55286 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -235,6 +235,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, instance_version = instance.data.get("version") # take this if exists if instance_version != 1: override_version = instance_version + output_dir = self._get_publish_folder( instance.context.data['anatomy'], deepcopy(instance.data["anatomyData"]), @@ -250,9 +251,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, self._create_metadata_path(instance) environment = { - "AVALON_PROJECT": legacy_io.Session["AVALON_PROJECT"], - "AVALON_ASSET": legacy_io.Session["AVALON_ASSET"], - "AVALON_TASK": legacy_io.Session["AVALON_TASK"], + "AVALON_PROJECT": instance.context.data["projectName"], + "AVALON_ASSET": instance.context.data["asset"], + "AVALON_TASK": instance.context.data["task"], "OPENPYPE_USERNAME": instance.context.data["user"], "OPENPYPE_PUBLISH_JOB": "1", "OPENPYPE_RENDER_JOB": "0", @@ -385,11 +386,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, self.log.info("Preparing to copy ...") start = instance.data.get("frameStart") end = instance.data.get("frameEnd") - project_name = legacy_io.active_project() # get latest version of subset # this will stop if subset wasn't published yet - project_name = legacy_io.active_project() + project_name = self.context.data["projectName"] version = get_last_version_by_subset_name( project_name, instance.data.get("subset"), @@ -468,7 +468,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, list of instances """ - task = os.environ["AVALON_TASK"] + task = self.context.data["task"] + host_name = self.context.data["hostName"] subset = instance_data["subset"] cameras = instance_data.get("cameras", []) instances = [] @@ -526,15 +527,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, self.log.info("Creating data for: {}".format(subset_name)) - app = os.environ.get("AVALON_APP", "") - if isinstance(col, list): render_file_name = os.path.basename(col[0]) else: render_file_name = os.path.basename(col) aov_patterns = self.aov_filter - preview = match_aov_pattern(app, aov_patterns, render_file_name) + preview = match_aov_pattern( + host_name, aov_patterns, render_file_name + ) # toggle preview on if multipart is on if instance_data.get("multipartExr"): @@ -624,7 +625,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, """ representations = [] - host_name = os.environ.get("AVALON_APP", "") + host_name = self.context.data["hostName"] collections, remainders = clique.assemble(exp_files) # create representation for every collected sequence @@ -790,7 +791,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, self.context = context self.anatomy = instance.context.data["anatomy"] - asset = data.get("asset") or legacy_io.Session["AVALON_ASSET"] + asset = data.get("asset") or context.data["asset"] subset = data.get("subset") start = instance.data.get("frameStart") @@ -1174,7 +1175,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, prev_start = None prev_end = None - project_name = legacy_io.active_project() + project_name = self.context.data["projectName"] version = get_last_version_by_subset_name( project_name, subset, @@ -1223,8 +1224,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, be stored based on 'publish' template """ + + project_name = self.context.data["projectName"] if not version: - project_name = legacy_io.active_project() version = get_last_version_by_subset_name( project_name, subset, @@ -1247,7 +1249,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, else: # solve deprecated situation when `folder` key is not underneath # `publish` anatomy - project_name = legacy_io.Session["AVALON_PROJECT"] self.log.warning(( "Deprecation warning: Anatomy does not have set `folder`" " key underneath `publish` (in global of for project `{}`)." diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index e13b7e65cd..fe3275ce2c 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -1,8 +1,6 @@ import logging import pyblish.api -from openpype.pipeline import legacy_io - class CollectFtrackApi(pyblish.api.ContextPlugin): """ Collects an ftrack session and the current task id. """ @@ -24,9 +22,9 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): self.log.debug("Ftrack user: \"{0}\"".format(session.api_user)) # Collect task - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] + project_name = context.data["projectName"] + asset_name = context.data["asset"] + task_name = context.data["task"] # Find project entity project_query = 'Project where full_name is "{0}"'.format(project_name) diff --git a/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py b/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py index 65af90e8a6..42351b781b 100644 --- a/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py +++ b/openpype/modules/royalrender/plugins/publish/collect_sequences_from_job.py @@ -189,7 +189,7 @@ class CollectSequencesFromJob(pyblish.api.ContextPlugin): "families": list(families), "subset": subset, "asset": data.get( - "asset", legacy_io.Session["AVALON_ASSET"] + "asset", context.data["asset"] ), "stagingDir": root, "frameStart": start, diff --git a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py index 0b03ac2e5d..43f5d1ef0e 100644 --- a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py +++ b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py @@ -14,7 +14,7 @@ class CollectShotgridEntities(pyblish.api.ContextPlugin): avalon_project = context.data.get("projectEntity") avalon_asset = context.data.get("assetEntity") - avalon_task_name = os.getenv("AVALON_TASK") + avalon_task_name = context.data.get("task") self.log.info(avalon_project) self.log.info(avalon_asset) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index d656d58adc..5c15a5fa82 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -88,6 +88,7 @@ from .context_tools import ( deregister_host, get_process_id, + get_global_context, get_current_context, get_current_host_name, get_current_project_name, @@ -186,6 +187,7 @@ __all__ = ( "deregister_host", "get_process_id", + "get_global_context", "get_current_context", "get_current_host_name", "get_current_project_name", diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 97a5c1ba69..c12b76cc74 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -320,7 +320,7 @@ def get_current_host_name(): """Current host name. Function is based on currently registered host integration or environment - variant 'AVALON_APP'. + variable 'AVALON_APP'. Returns: Union[str, None]: Name of host integration in current process or None. @@ -333,6 +333,26 @@ def get_current_host_name(): def get_global_context(): + """Global context defined in environment variables. + + Values here may not reflect current context of host integration. The + function can be used on startup before a host is registered. + + Use 'get_current_context' to make sure you'll get current host integration + context info. + + Example: + { + "project_name": "Commercial", + "asset_name": "Bunny", + "task_name": "Animation", + } + + Returns: + dict[str, Union[str, None]]: Context defined with environment + variables. + """ + return { "project_name": os.environ.get("AVALON_PROJECT"), "asset_name": os.environ.get("AVALON_ASSET"), diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 947a90ef08..c9edbbfd71 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -660,12 +660,12 @@ def discover_convertor_plugins(*args, **kwargs): def discover_legacy_creator_plugins(): - from openpype.lib import Logger + from openpype.pipeline import get_current_project_name log = Logger.get_logger("CreatorDiscover") plugins = discover(LegacyCreator) - project_name = os.environ.get("AVALON_PROJECT") + project_name = get_current_project_name() system_settings = get_system_settings() project_settings = get_project_settings(project_name) for plugin in plugins: diff --git a/openpype/pipeline/template_data.py b/openpype/pipeline/template_data.py index 627eba5c3d..fd21930ecc 100644 --- a/openpype/pipeline/template_data.py +++ b/openpype/pipeline/template_data.py @@ -128,7 +128,7 @@ def get_task_template_data(project_doc, asset_doc, task_name): Args: project_doc (Dict[str, Any]): Queried project document. asset_doc (Dict[str, Any]): Queried asset document. - tas_name (str): Name of task for which data should be returned. + task_name (str): Name of task for which data should be returned. Returns: Dict[str, Dict[str, str]]: Template data diff --git a/openpype/pipeline/workfile/build_workfile.py b/openpype/pipeline/workfile/build_workfile.py index 8329487839..7b153d37b9 100644 --- a/openpype/pipeline/workfile/build_workfile.py +++ b/openpype/pipeline/workfile/build_workfile.py @@ -9,7 +9,6 @@ from '~/openpype/pipeline/workfile/workfile_template_builder'. Which gives more abilities to define how build happens but require more code to achive it. """ -import os import re import collections import json @@ -26,7 +25,6 @@ from openpype.lib import ( filter_profiles, Logger, ) -from openpype.pipeline import legacy_io from openpype.pipeline.load import ( discover_loader_plugins, IncompatibleLoaderError, @@ -102,11 +100,17 @@ class BuildWorkfile: List[Dict[str, Any]]: Loaded containers during build. """ + from openpype.pipeline.context_tools import ( + get_current_project_name, + get_current_asset_name, + get_current_task_name, + ) + loaded_containers = [] # Get current asset name and entity - project_name = legacy_io.active_project() - current_asset_name = legacy_io.Session["AVALON_ASSET"] + project_name = get_current_project_name() + current_asset_name = get_current_asset_name() current_asset_entity = get_asset_by_name( project_name, current_asset_name ) @@ -135,7 +139,7 @@ class BuildWorkfile: return loaded_containers # Get current task name - current_task_name = legacy_io.Session["AVALON_TASK"] + current_task_name = get_current_task_name() # Load workfile presets for task self.build_presets = self.get_build_presets( @@ -236,9 +240,14 @@ class BuildWorkfile: Dict[str, Any]: preset per entered task name """ - host_name = os.environ["AVALON_APP"] + from openpype.pipeline.context_tools import ( + get_current_host_name, + get_current_project_name, + ) + + host_name = get_current_host_name() project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] + get_current_project_name() ) host_settings = project_settings.get(host_name) or {} @@ -651,13 +660,15 @@ class BuildWorkfile: ``` """ + from openpype.pipeline.context_tools import get_current_project_name + output = {} if not asset_docs: return output asset_docs_by_ids = {asset["_id"]: asset for asset in asset_docs} - project_name = legacy_io.active_project() + project_name = get_current_project_name() subsets = list(get_subsets( project_name, asset_ids=asset_docs_by_ids.keys() )) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index e1013b2645..bdb13415bf 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -28,8 +28,7 @@ from openpype.settings import ( get_project_settings, get_system_settings, ) -from openpype.host import IWorkfileHost -from openpype.host import HostBase +from openpype.host import IWorkfileHost, HostBase from openpype.lib import ( Logger, StringTemplate, @@ -37,7 +36,7 @@ from openpype.lib import ( attribute_definitions, ) from openpype.lib.attribute_definitions import get_attributes_keys -from openpype.pipeline import legacy_io, Anatomy +from openpype.pipeline import Anatomy from openpype.pipeline.load import ( get_loaders_by_name, get_contexts_for_repre_docs, @@ -125,15 +124,30 @@ class AbstractTemplateBuilder(object): @property def project_name(self): - return legacy_io.active_project() + if isinstance(self._host, HostBase): + return self._host.get_current_project_name() + return os.getenv("AVALON_PROJECT") @property def current_asset_name(self): - return legacy_io.Session["AVALON_ASSET"] + if isinstance(self._host, HostBase): + return self._host.get_current_asset_name() + return os.getenv("AVALON_ASSET") @property def current_task_name(self): - return legacy_io.Session["AVALON_TASK"] + if isinstance(self._host, HostBase): + return self._host.get_current_task_name() + return os.getenv("AVALON_TASK") + + def get_current_context(self): + if isinstance(self._host, HostBase): + return self._host.get_current_context() + return { + "project_name": self.project_name, + "asset_name": self.current_asset_name, + "task_name": self.current_task_name + } @property def system_settings(self): @@ -790,10 +804,9 @@ class AbstractTemplateBuilder(object): fill_data["root"] = anatomy.roots fill_data["project"] = { "name": project_name, - "code": anatomy["attributes"]["code"] + "code": anatomy.project_code, } - result = StringTemplate.format_template(path, fill_data) if result.solved: path = result.normalized() @@ -1705,9 +1718,10 @@ class PlaceholderCreateMixin(object): creator_plugin = self.builder.get_creators_by_name()[creator_name] # create subset name - project_name = legacy_io.Session["AVALON_PROJECT"] - task_name = legacy_io.Session["AVALON_TASK"] - asset_name = legacy_io.Session["AVALON_ASSET"] + context = self._builder.get_current_context() + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] if legacy_create: asset_doc = get_asset_by_name( diff --git a/openpype/plugins/publish/collect_current_context.py b/openpype/plugins/publish/collect_current_context.py index 7e42700d7d..166d75e5de 100644 --- a/openpype/plugins/publish/collect_current_context.py +++ b/openpype/plugins/publish/collect_current_context.py @@ -6,7 +6,7 @@ Provides: """ import pyblish.api -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_context class CollectCurrentContext(pyblish.api.ContextPlugin): @@ -19,24 +19,20 @@ class CollectCurrentContext(pyblish.api.ContextPlugin): label = "Collect Current context" def process(self, context): - # Make sure 'legacy_io' is intalled - legacy_io.install() - # Check if values are already set project_name = context.data.get("projectName") asset_name = context.data.get("asset") task_name = context.data.get("task") + + current_context = get_current_context() if not project_name: - project_name = legacy_io.current_project() - context.data["projectName"] = project_name + context.data["projectName"] = current_context["project_name"] if not asset_name: - asset_name = legacy_io.Session.get("AVALON_ASSET") - context.data["asset"] = asset_name + context.data["asset"] = current_context["asset_name"] if not task_name: - task_name = legacy_io.Session.get("AVALON_TASK") - context.data["task"] = task_name + context.data["task"] = current_context["task_name"] # QUESTION should we be explicit with keys? (the same on instances) # - 'asset' -> 'assetName' diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 042324ef8b..10f755ae77 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -90,7 +90,10 @@ class PypeCommands: from openpype.lib import Logger from openpype.lib.applications import get_app_environments_for_context from openpype.modules import ModulesManager - from openpype.pipeline import install_openpype_plugins + from openpype.pipeline import ( + install_openpype_plugins, + get_global_context, + ) from openpype.tools.utils.host_tools import show_publish from openpype.tools.utils.lib import qt_app_context @@ -112,12 +115,14 @@ class PypeCommands: if not any(paths): raise RuntimeError("No publish paths specified") - if os.getenv("AVALON_APP_NAME"): + app_full_name = os.getenv("AVALON_APP_NAME") + if app_full_name: + context = get_global_context() env = get_app_environments_for_context( - os.environ["AVALON_PROJECT"], - os.environ["AVALON_ASSET"], - os.environ["AVALON_TASK"], - os.environ["AVALON_APP_NAME"] + context["project_name"], + context["asset_name"], + context["task_name"], + app_full_name ) os.environ.update(env) diff --git a/openpype/scripts/fusion_switch_shot.py b/openpype/scripts/fusion_switch_shot.py index fc22f060a2..8ecf4fb5ea 100644 --- a/openpype/scripts/fusion_switch_shot.py +++ b/openpype/scripts/fusion_switch_shot.py @@ -15,6 +15,7 @@ from openpype.pipeline import ( install_host, registered_host, legacy_io, + get_current_project_name, ) from openpype.pipeline.context_tools import get_workdir_from_session @@ -130,7 +131,7 @@ def update_frame_range(comp, representations): """ version_ids = [r["parent"] for r in representations] - project_name = legacy_io.active_project() + project_name = get_current_project_name() versions = list(get_versions(project_name, version_ids=version_ids)) start = min(v["data"]["frameStart"] for v in versions) @@ -161,7 +162,7 @@ def switch(asset_name, filepath=None, new=True): # Assert asset name exists # It is better to do this here then to wait till switch_shot does it - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset = get_asset_by_name(project_name, asset_name) assert asset, "Could not find '%s' in the database" % asset_name diff --git a/openpype/tools/adobe_webserver/app.py b/openpype/tools/adobe_webserver/app.py index 3911baf7ac..49d61d3883 100644 --- a/openpype/tools/adobe_webserver/app.py +++ b/openpype/tools/adobe_webserver/app.py @@ -16,7 +16,7 @@ from wsrpc_aiohttp import ( WSRPCClient ) -from openpype.pipeline import legacy_io +from openpype.pipeline import get_global_context log = logging.getLogger(__name__) @@ -80,9 +80,10 @@ class WebServerTool: loop=asyncio.get_event_loop()) await client.connect() - project = legacy_io.Session["AVALON_PROJECT"] - asset = legacy_io.Session["AVALON_ASSET"] - task = legacy_io.Session["AVALON_TASK"] + context = get_global_context() + project = context["project_name"] + asset = context["asset_name"] + task = context["task_name"] log.info("Sending context change to {}-{}-{}".format(project, asset, task)) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index 57e2c49576..47f27a262a 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -8,7 +8,11 @@ from openpype.client import get_asset_by_name, get_subsets from openpype import style from openpype.settings import get_current_project_settings from openpype.tools.utils.lib import qt_app_context -from openpype.pipeline import legacy_io +from openpype.pipeline import ( + get_current_project_name, + get_current_asset_name, + get_current_task_name, +) from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, legacy_create, @@ -216,7 +220,7 @@ class CreatorWindow(QtWidgets.QDialog): self._set_valid_state(False) return - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset_doc = None if creator_plugin: # Get the asset from the database which match with the name @@ -237,7 +241,7 @@ class CreatorWindow(QtWidgets.QDialog): return asset_id = asset_doc["_id"] - task_name = legacy_io.Session["AVALON_TASK"] + task_name = get_current_task_name() # Calculate subset name with Creator plugin subset_name = creator_plugin.get_subset_name( @@ -369,7 +373,7 @@ class CreatorWindow(QtWidgets.QDialog): self.setStyleSheet(style.load_stylesheet()) def refresh(self): - self._asset_name_input.setText(legacy_io.Session["AVALON_ASSET"]) + self._asset_name_input.setText(get_current_asset_name()) self._creators_model.reset() @@ -382,7 +386,7 @@ class CreatorWindow(QtWidgets.QDialog): ) current_index = None family = None - task_name = legacy_io.Session.get("AVALON_TASK", None) + task_name = get_current_task_name() or None lowered_task_name = task_name.lower() if task_name: for _family, _task_names in pype_project_setting.items(): diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 815ecf9efe..1cfcd0d8c0 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -15,7 +15,7 @@ from openpype.client import ( get_representation_by_id, ) from openpype.pipeline import ( - legacy_io, + get_current_project_name, schema, HeroVersionType, registered_host, @@ -62,7 +62,7 @@ class InventoryModel(TreeModel): if not self.sync_enabled: return - project_name = legacy_io.current_project() + project_name = get_current_project_name() active_site = sync_server.get_active_site(project_name) remote_site = sync_server.get_remote_site(project_name) @@ -320,7 +320,7 @@ class InventoryModel(TreeModel): """ # NOTE: @iLLiCiTiT this need refactor - project_name = legacy_io.active_project() + project_name = get_current_project_name() self.beginResetModel() diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ac242d24d2..bc4b7867c2 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -10,7 +10,7 @@ from openpype.host import IWorkfileHost, ILoadHost from openpype.lib import Logger from openpype.pipeline import ( registered_host, - legacy_io, + get_current_asset_name, ) from .lib import qt_app_context @@ -96,7 +96,7 @@ class HostToolsHelper: use_context = False if use_context: - context = {"asset": legacy_io.Session["AVALON_ASSET"]} + context = {"asset": get_current_asset_name()} loader_tool.set_context(context, refresh=True) else: loader_tool.refresh() diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 7b3faddf08..885df15da9 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -18,7 +18,11 @@ from openpype.style import ( from openpype.resources import get_image_path from openpype.lib import filter_profiles, Logger from openpype.settings import get_project_settings -from openpype.pipeline import registered_host +from openpype.pipeline import ( + registered_host, + get_current_context, + get_current_host_name, +) from .constants import CHECKED_INT, UNCHECKED_INT @@ -46,7 +50,6 @@ def checkstate_enum_to_int(state): return 2 - def center_window(window): """Move window to center of it's screen.""" @@ -496,10 +499,11 @@ class FamilyConfigCache: return # Update the icons from the project configuration - project_name = os.environ.get("AVALON_PROJECT") - asset_name = os.environ.get("AVALON_ASSET") - task_name = os.environ.get("AVALON_TASK") - host_name = os.environ.get("AVALON_APP") + context = get_current_context() + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] + host_name = get_current_host_name() if not all((project_name, asset_name, task_name)): return diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 2f338cf516..e4715a0340 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -21,6 +21,7 @@ from openpype.pipeline import ( registered_host, legacy_io, Anatomy, + get_current_project_name, ) from openpype.pipeline.context_tools import ( compute_session_changes, @@ -99,7 +100,7 @@ class FilesWidget(QtWidgets.QWidget): self._task_type = None # Pype's anatomy object for current project - project_name = legacy_io.Session["AVALON_PROJECT"] + project_name = get_current_project_name() self.anatomy = Anatomy(project_name) self.project_name = project_name # Template key used to get work template from anatomy templates diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 53f8894665..50c39d4a40 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -15,7 +15,12 @@ from openpype.client.operations import ( ) from openpype import style from openpype import resources -from openpype.pipeline import Anatomy +from openpype.pipeline import ( + Anatomy, + get_current_project_name, + get_current_asset_name, + get_current_task_name, +) from openpype.pipeline import legacy_io from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget from openpype.tools.utils.tasks_widget import TasksWidget @@ -285,8 +290,8 @@ class Window(QtWidgets.QWidget): if use_context is None or use_context is True: context = { - "asset": legacy_io.Session["AVALON_ASSET"], - "task": legacy_io.Session["AVALON_TASK"] + "asset": get_current_asset_name(), + "task": get_current_task_name() } self.set_context(context) @@ -296,7 +301,7 @@ class Window(QtWidgets.QWidget): @property def project_name(self): - return legacy_io.Session["AVALON_PROJECT"] + return get_current_project_name() def showEvent(self, event): super(Window, self).showEvent(event) @@ -325,7 +330,7 @@ class Window(QtWidgets.QWidget): workfile_doc = None if asset_id and task_name and filepath: filename = os.path.split(filepath)[1] - project_name = legacy_io.active_project() + project_name = self.project_name workfile_doc = get_workfile_info( project_name, asset_id, task_name, filename ) @@ -356,7 +361,7 @@ class Window(QtWidgets.QWidget): if not update_data: return - project_name = legacy_io.active_project() + project_name = self.project_name session = OperationsSession() session.update_entity( @@ -373,7 +378,7 @@ class Window(QtWidgets.QWidget): return filename = os.path.split(filepath)[1] - project_name = legacy_io.active_project() + project_name = self.project_name return get_workfile_info( project_name, asset_id, task_name, filename ) @@ -385,7 +390,7 @@ class Window(QtWidgets.QWidget): workdir, filename = os.path.split(filepath) - project_name = legacy_io.active_project() + project_name = self.project_name asset_id = self.assets_widget.get_selected_asset_id() task_name = self.tasks_widget.get_selected_task_name() diff --git a/poetry.lock b/poetry.lock index ee7003d565..f915832fb8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -302,6 +302,24 @@ files = [ pycodestyle = ">=2.10.0" tomli = {version = "*", markers = "python_version < \"3.11\""} +[[package]] +name = "ayon-python-api" +version = "0.1.16" +description = "AYON Python API" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "ayon-python-api-0.1.16.tar.gz", hash = "sha256:666110954dd75b2be1699a29b4732cfb0bcb09d01f64fba4449bfc8ac1fb43f1"}, + {file = "ayon_python_api-0.1.16-py3-none-any.whl", hash = "sha256:bbcd6df1f80ddf32e653a1bb31289cb5fd1a8bea36ab4c8e6aef08c41b6393de"}, +] + +[package.dependencies] +appdirs = ">=1,<2" +requests = ">=2.27.1" +six = ">=1.15" +Unidecode = ">=1.2.0" + [[package]] name = "babel" version = "2.11.0" diff --git a/pyproject.toml b/pyproject.toml index 32982bed8d..51895c20d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" +ayon-python-api = "^0.1" opencolorio = "^2.2.0" Unidecode = "^1.2" From 1597306f459b3b3d01289a1563c96f8e3e5bedb9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Apr 2023 10:23:58 +0200 Subject: [PATCH 211/446] Remove legacy integrator (#4786) --- openpype/plugins/publish/integrate.py | 39 - openpype/plugins/publish/integrate_legacy.py | 1299 ----------------- .../defaults/project_settings/global.json | 3 - .../schemas/schema_global_publish.json | 31 - 4 files changed, 1372 deletions(-) delete mode 100644 openpype/plugins/publish/integrate_legacy.py diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index f392cf67f7..e76f9ce9c4 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -148,14 +148,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "username", "user", "output" ] - skip_host_families = [] def process(self, instance): - if self._temp_skip_instance_by_settings(instance): - return - - # Mark instance as processed for legacy integrator - instance.data["processedWithNewIntegrator"] = True # Instance should be integrated on a farm if instance.data.get("farm"): @@ -201,39 +195,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() - def _temp_skip_instance_by_settings(self, instance): - """Decide if instance will be processed with new or legacy integrator. - - This is temporary solution until we test all usecases with new (this) - integrator plugin. - """ - - host_name = instance.context.data["hostName"] - instance_family = instance.data["family"] - instance_families = set(instance.data.get("families") or []) - - skip = False - for item in self.skip_host_families: - if host_name not in item["host"]: - continue - - families = set(item["families"]) - if instance_family in families: - skip = True - break - - for family in instance_families: - if family in families: - skip = True - break - - if skip: - break - - if skip: - self.log.debug("Instance is marked to be skipped by settings.") - return skip - def filter_representations(self, instance): # Prepare repsentations that should be integrated repres = instance.data.get("representations") diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py deleted file mode 100644 index c238cca633..0000000000 --- a/openpype/plugins/publish/integrate_legacy.py +++ /dev/null @@ -1,1299 +0,0 @@ -import os -from os.path import getsize -import logging -import sys -import copy -import clique -import errno -import six -import re -import shutil -from collections import deque, defaultdict -from datetime import datetime - -from bson.objectid import ObjectId -from pymongo import DeleteOne, InsertOne -import pyblish.api - -from openpype.client import ( - get_asset_by_name, - get_subset_by_id, - get_subset_by_name, - get_version_by_id, - get_version_by_name, - get_representations, - get_archived_representations, -) -from openpype.lib import ( - prepare_template_data, - create_hard_link, - StringTemplate, - TemplateUnsolved, - source_hash, - filter_profiles, - get_local_site_id, -) -from openpype.pipeline import legacy_io -from openpype.pipeline.publish import get_publish_template_name - -# this is needed until speedcopy for linux is fixed -if sys.platform == "win32": - from speedcopy import copyfile -else: - from shutil import copyfile - -log = logging.getLogger(__name__) - - -class IntegrateAssetNew(pyblish.api.InstancePlugin): - """Resolve any dependency issues - - This plug-in resolves any paths which, if not updated might break - the published file. - - The order of families is important, when working with lookdev you want to - first publish the texture, update the texture paths in the nodes and then - publish the shading network. Same goes for file dependent assets. - - Requirements for instance to be correctly integrated - - instance.data['representations'] - must be a list and each member - must be a dictionary with following data: - 'files': list of filenames for sequence, string for single file. - Only the filename is allowed, without the folder path. - 'stagingDir': "path/to/folder/with/files" - 'name': representation name (usually the same as extension) - 'ext': file extension - optional data - "frameStart" - "frameEnd" - 'fps' - "data": additional metadata for each representation. - """ - - label = "Integrate Asset (legacy)" - # Make sure it happens after new integrator - order = pyblish.api.IntegratorOrder + 0.00001 - families = ["workfile", - "pointcache", - "pointcloud", - "proxyAbc", - "camera", - "animation", - "model", - "maxScene", - "mayaAscii", - "mayaScene", - "setdress", - "layout", - "ass", - "vdbcache", - "scene", - "vrayproxy", - "vrayscene_layer", - "render", - "prerender", - "imagesequence", - "review", - "rendersetup", - "rig", - "plate", - "look", - "audio", - "yetiRig", - "yeticache", - "nukenodes", - "gizmo", - "source", - "matchmove", - "image", - "assembly", - "fbx", - "gltf", - "textures", - "action", - "harmony.template", - "harmony.palette", - "editorial", - "background", - "camerarig", - "redshiftproxy", - "effect", - "xgen", - "hda", - "usd", - "staticMesh", - "skeletalMesh", - "mvLook", - "mvUsdComposition", - "mvUsdOverride", - "simpleUnrealTexture" - ] - exclude_families = ["render.farm"] - db_representation_context_keys = [ - "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "task", "username", "user" - ] - default_template_name = "publish" - - # suffix to denote temporary files, use without '.' - TMP_FILE_EXT = 'tmp' - - # file_url : file_size of all published and uploaded files - integrated_file_sizes = {} - - # Attributes set by settings - subset_grouping_profiles = None - - def process(self, instance): - if instance.data.get("processedWithNewIntegrator"): - self.log.debug( - "Instance was already processed with new integrator" - ) - return - - for ef in self.exclude_families: - if ( - instance.data["family"] == ef or - ef in instance.data["families"]): - self.log.debug("Excluded family '{}' in '{}' or {}".format( - ef, instance.data["family"], instance.data["families"])) - return - - # instance should be published on a farm - if instance.data.get("farm"): - return - - # Prepare repsentations that should be integrated - repres = instance.data.get("representations") - # Raise error if instance don't have any representations - if not repres: - raise ValueError( - "Instance {} has no files to transfer".format( - instance.data["family"] - ) - ) - - # Validate type of stored representations - if not isinstance(repres, (list, tuple)): - raise TypeError( - "Instance 'files' must be a list, got: {0} {1}".format( - str(type(repres)), str(repres) - ) - ) - - # Filter representations - filtered_repres = [] - for repre in repres: - if "delete" in repre.get("tags", []): - continue - filtered_repres.append(repre) - - # Skip instance if there are not representations to integrate - # all representations should not be integrated - if not filtered_repres: - self.log.warning(( - "Skipping, there are no representations" - " to integrate for instance {}" - ).format(instance.data["family"])) - return - - self.integrated_file_sizes = {} - try: - self.register(instance, filtered_repres) - self.log.info("Integrated Asset in to the database ...") - self.log.info("instance.data: {}".format(instance.data)) - self.handle_destination_files(self.integrated_file_sizes, - 'finalize') - except Exception: - # clean destination - self.log.critical("Error when registering", exc_info=True) - self.handle_destination_files(self.integrated_file_sizes, 'remove') - six.reraise(*sys.exc_info()) - - def register(self, instance, repres): - # Required environment variables - anatomy_data = instance.data["anatomyData"] - - legacy_io.install() - - context = instance.context - - project_entity = instance.data["projectEntity"] - project_name = project_entity["name"] - - context_asset_name = None - context_asset_doc = context.data.get("assetEntity") - if context_asset_doc: - context_asset_name = context_asset_doc["name"] - - asset_name = instance.data["asset"] - asset_entity = instance.data.get("assetEntity") - if not asset_entity or asset_entity["name"] != context_asset_name: - asset_entity = get_asset_by_name(project_name, asset_name) - assert asset_entity, ( - "No asset found by the name \"{0}\" in project \"{1}\"" - ).format(asset_name, project_entity["name"]) - - instance.data["assetEntity"] = asset_entity - - # update anatomy data with asset specific keys - # - name should already been set - hierarchy = "" - parents = asset_entity["data"]["parents"] - if parents: - hierarchy = "/".join(parents) - anatomy_data["hierarchy"] = hierarchy - - # Make sure task name in anatomy data is same as on instance.data - asset_tasks = ( - asset_entity.get("data", {}).get("tasks") - ) or {} - task_name = instance.data.get("task") - if task_name: - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") - - project_task_types = project_entity["config"]["tasks"] - task_code = project_task_types.get(task_type, {}).get("short_name") - anatomy_data["task"] = { - "name": task_name, - "type": task_type, - "short": task_code - } - - elif "task" in anatomy_data: - # Just set 'task_name' variable to context task - task_name = anatomy_data["task"]["name"] - task_type = anatomy_data["task"]["type"] - - else: - task_name = None - task_type = None - - # Fill family in anatomy data - anatomy_data["family"] = instance.data.get("family") - - stagingdir = instance.data.get("stagingDir") - if not stagingdir: - self.log.debug(( - "{0} is missing reference to staging directory." - " Will try to get it from representation." - ).format(instance)) - - else: - self.log.debug( - "Establishing staging directory @ {0}".format(stagingdir) - ) - - subset = self.get_subset(project_name, asset_entity, instance) - instance.data["subsetEntity"] = subset - - version_number = instance.data["version"] - self.log.debug("Next version: v{}".format(version_number)) - - version_data = self.create_version_data(context, instance) - - version_data_instance = instance.data.get('versionData') - if version_data_instance: - version_data.update(version_data_instance) - - # TODO rename method from `create_version` to - # `prepare_version` or similar... - version = self.create_version( - subset=subset, - version_number=version_number, - data=version_data - ) - - self.log.debug("Creating version ...") - - new_repre_names_low = [ - _repre["name"].lower() - for _repre in repres - ] - - existing_version = get_version_by_name( - project_name, version_number, subset["_id"] - ) - - if existing_version is None: - version_id = legacy_io.insert_one(version).inserted_id - else: - # Check if instance have set `append` mode which cause that - # only replicated representations are set to archive - append_repres = instance.data.get("append", False) - - # Update version data - # TODO query by _id and - legacy_io.update_many({ - 'type': 'version', - 'parent': subset["_id"], - 'name': version_number - }, { - '$set': version - }) - version_id = existing_version['_id'] - - # Find representations of existing version and archive them - current_repres = list(get_representations( - project_name, version_ids=[version_id] - )) - bulk_writes = [] - for repre in current_repres: - if append_repres: - # archive only duplicated representations - if repre["name"].lower() not in new_repre_names_low: - continue - # Representation must change type, - # `_id` must be stored to other key and replaced with new - # - that is because new representations should have same ID - repre_id = repre["_id"] - bulk_writes.append(DeleteOne({"_id": repre_id})) - - repre["orig_id"] = repre_id - repre["_id"] = ObjectId() - repre["type"] = "archived_representation" - bulk_writes.append(InsertOne(repre)) - - # bulk updates - if bulk_writes: - legacy_io.database[project_name].bulk_write( - bulk_writes - ) - - version = get_version_by_id(project_name, version_id) - instance.data["versionEntity"] = version - - existing_repres = list(get_archived_representations( - project_name, - version_ids=[version_id] - )) - - instance.data['version'] = version['name'] - - intent_value = instance.context.data.get("intent") - if intent_value and isinstance(intent_value, dict): - intent_value = intent_value.get("value") - - if intent_value: - anatomy_data["intent"] = intent_value - - anatomy = instance.context.data['anatomy'] - - # Find the representations to transfer amongst the files - # Each should be a single representation (as such, a single extension) - representations = [] - destination_list = [] - - orig_transfers = [] - if 'transfers' not in instance.data: - instance.data['transfers'] = [] - else: - orig_transfers = list(instance.data['transfers']) - - family = self.main_family_from_instance(instance) - - template_name = get_publish_template_name( - project_name, - instance.context.data["hostName"], - family, - task_name=task_info.get("name"), - task_type=task_info.get("type"), - project_settings=instance.context.data["project_settings"], - logger=self.log - ) - - published_representations = {} - for idx, repre in enumerate(repres): - published_files = [] - - # create template data for Anatomy - template_data = copy.deepcopy(anatomy_data) - if intent_value is not None: - template_data["intent"] = intent_value - - resolution_width = repre.get("resolutionWidth") - resolution_height = repre.get("resolutionHeight") - fps = instance.data.get("fps") - - if resolution_width: - template_data["resolution_width"] = resolution_width - if resolution_width: - template_data["resolution_height"] = resolution_height - if resolution_width: - template_data["fps"] = fps - - if "originalBasename" in instance.data: - template_data.update({ - "originalBasename": instance.data.get("originalBasename") - }) - - files = repre['files'] - if repre.get('stagingDir'): - stagingdir = repre['stagingDir'] - - if repre.get("outputName"): - template_data["output"] = repre['outputName'] - - template_data["representation"] = repre["name"] - - ext = repre["ext"] - if ext.startswith("."): - self.log.warning(( - "Implementaion warning: <\"{}\">" - " Representation's extension stored under \"ext\" key " - " started with dot (\"{}\")." - ).format(repre["name"], ext)) - ext = ext[1:] - repre["ext"] = ext - template_data["ext"] = ext - - self.log.info(template_name) - template = os.path.normpath( - anatomy.templates[template_name]["path"]) - - sequence_repre = isinstance(files, list) - repre_context = None - if sequence_repre: - self.log.debug( - "files: {}".format(files)) - src_collections, remainder = clique.assemble(files) - self.log.debug( - "src_tail_collections: {}".format(str(src_collections))) - src_collection = src_collections[0] - - # Assert that each member has identical suffix - src_head = src_collection.format("{head}") - src_tail = src_collection.format("{tail}") - - # fix dst_padding - valid_files = [x for x in files if src_collection.match(x)] - padd_len = len( - valid_files[0].replace(src_head, "").replace(src_tail, "") - ) - src_padding_exp = "%0{}d".format(padd_len) - - test_dest_files = list() - for i in [1, 2]: - template_data["representation"] = repre['ext'] - if not repre.get("udim"): - template_data["frame"] = src_padding_exp % i - else: - template_data["udim"] = src_padding_exp % i - - template_obj = anatomy.templates_obj[template_name]["path"] - template_filled = template_obj.format_strict(template_data) - if repre_context is None: - repre_context = template_filled.used_values - test_dest_files.append( - os.path.normpath(template_filled) - ) - if not repre.get("udim"): - template_data["frame"] = repre_context["frame"] - else: - template_data["udim"] = repre_context["udim"] - - self.log.debug( - "test_dest_files: {}".format(str(test_dest_files))) - - dst_collections, remainder = clique.assemble(test_dest_files) - dst_collection = dst_collections[0] - dst_head = dst_collection.format("{head}") - dst_tail = dst_collection.format("{tail}") - - index_frame_start = None - - # TODO use frame padding from right template group - if repre.get("frameStart") is not None: - frame_start_padding = int( - anatomy.templates["render"].get( - "frame_padding", - anatomy.templates["render"].get("padding") - ) - ) - - index_frame_start = int(repre.get("frameStart")) - - # exception for slate workflow - if index_frame_start and "slate" in instance.data["families"]: - index_frame_start -= 1 - - dst_padding_exp = src_padding_exp - dst_start_frame = None - collection_start = list(src_collection.indexes)[0] - for i in src_collection.indexes: - # TODO 1.) do not count padding in each index iteration - # 2.) do not count dst_padding from src_padding before - # index_frame_start check - frame_number = i - collection_start - src_padding = src_padding_exp % i - - src_file_name = "{0}{1}{2}".format( - src_head, src_padding, src_tail) - - dst_padding = src_padding_exp % frame_number - - if index_frame_start is not None: - dst_padding_exp = "%0{}d".format(frame_start_padding) - dst_padding = dst_padding_exp % (index_frame_start + frame_number) # noqa: E501 - elif repre.get("udim"): - dst_padding = int(i) - - dst = "{0}{1}{2}".format( - dst_head, - dst_padding, - dst_tail - ) - - self.log.debug("destination: `{}`".format(dst)) - src = os.path.join(stagingdir, src_file_name) - - self.log.debug("source: {}".format(src)) - instance.data["transfers"].append([src, dst]) - - published_files.append(dst) - - # for adding first frame into db - if not dst_start_frame: - dst_start_frame = dst_padding - - # Store used frame value to template data - if repre.get("frame"): - template_data["frame"] = dst_start_frame - - dst = "{0}{1}{2}".format( - dst_head, - dst_start_frame, - dst_tail - ) - repre['published_path'] = dst - - else: - # Single file - # _______ - # | |\ - # | | - # | | - # | | - # |_______| - # - template_data.pop("frame", None) - fname = files - assert not os.path.isabs(fname), ( - "Given file name is a full path" - ) - - template_data["representation"] = repre['ext'] - # Store used frame value to template data - if repre.get("udim"): - template_data["udim"] = repre["udim"][0] - src = os.path.join(stagingdir, fname) - template_obj = anatomy.templates_obj[template_name]["path"] - template_filled = template_obj.format_strict(template_data) - repre_context = template_filled.used_values - dst = os.path.normpath(template_filled) - - instance.data["transfers"].append([src, dst]) - - published_files.append(dst) - repre['published_path'] = dst - self.log.debug("__ dst: {}".format(dst)) - - if not instance.data.get("publishDir"): - instance.data["publishDir"] = ( - anatomy.templates_obj[template_name]["folder"] - .format_strict(template_data) - ) - if repre.get("udim"): - repre_context["udim"] = repre.get("udim") # store list - - repre["publishedFiles"] = published_files - - for key in self.db_representation_context_keys: - value = template_data.get(key) - if not value: - continue - repre_context[key] = template_data[key] - - # Use previous representation's id if there are any - repre_id = None - repre_name_low = repre["name"].lower() - for _repre in existing_repres: - # NOTE should we check lowered names? - if repre_name_low == _repre["name"]: - repre_id = _repre["orig_id"] - break - - # Create new id if existing representations does not match - if repre_id is None: - repre_id = ObjectId() - - data = repre.get("data") or {} - data.update({'path': dst, 'template': template}) - representation = { - "_id": repre_id, - "schema": "openpype:representation-2.0", - "type": "representation", - "parent": version_id, - "name": repre['name'], - "data": data, - "dependencies": instance.data.get("dependencies", "").split(), - - # Imprint shortcut to context - # for performance reasons. - "context": repre_context - } - - if repre.get("outputName"): - representation["context"]["output"] = repre['outputName'] - - if sequence_repre and repre.get("frameStart") is not None: - representation['context']['frame'] = ( - dst_padding_exp % int(repre.get("frameStart")) - ) - - # any file that should be physically copied is expected in - # 'transfers' or 'hardlinks' - if instance.data.get('transfers', False) or \ - instance.data.get('hardlinks', False): - # could throw exception, will be caught in 'process' - # all integration to DB is being done together lower, - # so no rollback needed - self.log.debug("Integrating source files to destination ...") - self.integrated_file_sizes.update(self.integrate(instance)) - self.log.debug("Integrated files {}". - format(self.integrated_file_sizes)) - - # get 'files' info for representation and all attached resources - self.log.debug("Preparing files information ...") - representation["files"] = self.get_files_info( - instance, - self.integrated_file_sizes) - - self.log.debug("__ representation: {}".format(representation)) - destination_list.append(dst) - self.log.debug("__ destination_list: {}".format(destination_list)) - instance.data['destination_list'] = destination_list - representations.append(representation) - published_representations[repre_id] = { - "representation": representation, - "anatomy_data": template_data, - "published_files": published_files - } - self.log.debug("__ representations: {}".format(representations)) - # reset transfers for next representation - # instance.data['transfers'] is used as a global variable - # in current codebase - instance.data['transfers'] = list(orig_transfers) - - # Remove old representations if there are any (before insertion of new) - if existing_repres: - repre_ids_to_remove = [] - for repre in existing_repres: - repre_ids_to_remove.append(repre["_id"]) - legacy_io.delete_many({"_id": {"$in": repre_ids_to_remove}}) - - for rep in instance.data["representations"]: - self.log.debug("__ rep: {}".format(rep)) - - legacy_io.insert_many(representations) - instance.data["published_representations"] = ( - published_representations - ) - # self.log.debug("Representation: {}".format(representations)) - self.log.info("Registered {} items".format(len(representations))) - - def integrate(self, instance): - """ Move the files. - - Through `instance.data["transfers"]` - - Args: - instance: the instance to integrate - Returns: - integrated_file_sizes: dictionary of destination file url and - its size in bytes - """ - # store destination url and size for reporting and rollback - integrated_file_sizes = {} - transfers = list(instance.data.get("transfers", list())) - for src, dest in transfers: - if os.path.normpath(src) != os.path.normpath(dest): - dest = self.get_dest_temp_url(dest) - self.copy_file(src, dest) - # TODO needs to be updated during site implementation - integrated_file_sizes[dest] = os.path.getsize(dest) - - # Produce hardlinked copies - # Note: hardlink can only be produced between two files on the same - # server/disk and editing one of the two will edit both files at once. - # As such it is recommended to only make hardlinks between static files - # to ensure publishes remain safe and non-edited. - hardlinks = instance.data.get("hardlinks", list()) - for src, dest in hardlinks: - dest = self.get_dest_temp_url(dest) - self.log.debug("Hardlinking file ... {} -> {}".format(src, dest)) - if not os.path.exists(dest): - self.hardlink_file(src, dest) - - # TODO needs to be updated during site implementation - integrated_file_sizes[dest] = os.path.getsize(dest) - - return integrated_file_sizes - - def copy_file(self, src, dst): - """ Copy given source to destination - - Arguments: - src (str): the source file which needs to be copied - dst (str): the destination of the sourc file - Returns: - None - """ - src = os.path.normpath(src) - dst = os.path.normpath(dst) - self.log.debug("Copying file ... {} -> {}".format(src, dst)) - dirname = os.path.dirname(dst) - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - # copy file with speedcopy and check if size of files are simetrical - while True: - if not shutil._samefile(src, dst): - copyfile(src, dst) - else: - self.log.critical( - "files are the same {} to {}".format(src, dst) - ) - os.remove(dst) - try: - shutil.copyfile(src, dst) - self.log.debug("Copying files with shutil...") - except OSError as e: - self.log.critical("Cannot copy {} to {}".format(src, dst)) - self.log.critical(e) - six.reraise(*sys.exc_info()) - if str(getsize(src)) in str(getsize(dst)): - break - - def hardlink_file(self, src, dst): - dirname = os.path.dirname(dst) - - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - create_hard_link(src, dst) - - def get_subset(self, project_name, asset, instance): - subset_name = instance.data["subset"] - subset = get_subset_by_name(project_name, subset_name, asset["_id"]) - - if subset is None: - self.log.info("Subset '%s' not found, creating ..." % subset_name) - self.log.debug("families. %s" % instance.data.get('families')) - self.log.debug( - "families. %s" % type(instance.data.get('families'))) - - family = instance.data.get("family") - families = [] - if family: - families.append(family) - - for _family in (instance.data.get("families") or []): - if _family not in families: - families.append(_family) - - _id = legacy_io.insert_one({ - "schema": "openpype:subset-3.0", - "type": "subset", - "name": subset_name, - "data": { - "families": families - }, - "parent": asset["_id"] - }).inserted_id - - subset = get_subset_by_id(project_name, _id) - - # QUESTION Why is changing of group and updating it's - # families in 'get_subset'? - self._set_subset_group(instance, subset["_id"]) - - # Update families on subset. - families = [instance.data["family"]] - families.extend(instance.data.get("families", [])) - legacy_io.update_many( - {"type": "subset", "_id": ObjectId(subset["_id"])}, - {"$set": {"data.families": families}} - ) - - return subset - - def _set_subset_group(self, instance, subset_id): - """ - Mark subset as belonging to group in DB. - - Uses Settings > Global > Publish plugins > IntegrateAssetNew - - Args: - instance (dict): processed instance - subset_id (str): DB's subset _id - - """ - # Fist look into instance data - subset_group = instance.data.get("subsetGroup") - if not subset_group: - subset_group = self._get_subset_group(instance) - - if subset_group: - legacy_io.update_many({ - 'type': 'subset', - '_id': ObjectId(subset_id) - }, {'$set': {'data.subsetGroup': subset_group}}) - - def _get_subset_group(self, instance): - """Look into subset group profiles set by settings. - - Attribute 'subset_grouping_profiles' is defined by OpenPype settings. - """ - # Skip if 'subset_grouping_profiles' is empty - if not self.subset_grouping_profiles: - return None - - # QUESTION - # - is there a chance that task name is not filled in anatomy - # data? - # - should we use context task in that case? - anatomy_data = instance.data["anatomyData"] - task_name = None - task_type = None - if "task" in anatomy_data: - task_name = anatomy_data["task"]["name"] - task_type = anatomy_data["task"]["type"] - filtering_criteria = { - "families": instance.data["family"], - "hosts": instance.context.data["hostName"], - "tasks": task_name, - "task_types": task_type - } - matching_profile = filter_profiles( - self.subset_grouping_profiles, - filtering_criteria - ) - # Skip if there is not matchin profile - if not matching_profile: - return None - - filled_template = None - template = matching_profile["template"] - fill_pairs = ( - ("family", filtering_criteria["families"]), - ("task", filtering_criteria["tasks"]), - ("host", filtering_criteria["hosts"]), - ("subset", instance.data["subset"]), - ("renderlayer", instance.data.get("renderlayer")) - ) - fill_pairs = prepare_template_data(fill_pairs) - - try: - filled_template = StringTemplate.format_strict_template( - template, fill_pairs - ) - except (KeyError, TemplateUnsolved): - keys = [] - if fill_pairs: - keys = fill_pairs.keys() - - msg = "Subset grouping failed. " \ - "Only {} are expected in Settings".format(','.join(keys)) - self.log.warning(msg) - - return filled_template - - def create_version(self, subset, version_number, data=None): - """ Copy given source to destination - - Args: - subset (dict): the registered subset of the asset - version_number (int): the version number - - Returns: - dict: collection of data to create a version - """ - - return {"schema": "openpype:version-3.0", - "type": "version", - "parent": subset["_id"], - "name": version_number, - "data": data} - - def create_version_data(self, context, instance): - """Create the data collection for the version - - Args: - context: the current context - instance: the current instance being published - - Returns: - dict: the required information with instance.data as key - """ - - families = [] - current_families = instance.data.get("families", list()) - instance_family = instance.data.get("family", None) - - if instance_family is not None: - families.append(instance_family) - families += current_families - - # create relative source path for DB - source = instance.data.get("source") - if not source: - source = context.data["currentFile"] - anatomy = instance.context.data["anatomy"] - source = self.get_rootless_path(anatomy, source) - - self.log.debug("Source: {}".format(source)) - version_data = { - "families": families, - "time": context.data["time"], - "author": context.data["user"], - "source": source, - "comment": instance.data["comment"], - "machine": context.data.get("machine"), - "fps": context.data.get( - "fps", instance.data.get("fps") - ) - } - - intent_value = instance.context.data.get("intent") - if intent_value and isinstance(intent_value, dict): - intent_value = intent_value.get("value") - - if intent_value: - version_data["intent"] = intent_value - - # Include optional data if present in - optionals = [ - "frameStart", "frameEnd", "step", - "handleEnd", "handleStart", "sourceHashes" - ] - for key in optionals: - if key in instance.data: - version_data[key] = instance.data[key] - - return version_data - - def main_family_from_instance(self, instance): - """Returns main family of entered instance.""" - family = instance.data.get("family") - if not family: - family = instance.data["families"][0] - return family - - def get_rootless_path(self, anatomy, path): - """ Returns, if possible, path without absolute portion from host - (eg. 'c:\' or '/opt/..') - This information is host dependent and shouldn't be captured. - Example: - 'c:/projects/MyProject1/Assets/publish...' > - '{root}/MyProject1/Assets...' - - Args: - anatomy: anatomy part from instance - path: path (absolute) - Returns: - path: modified path if possible, or unmodified path - + warning logged - """ - success, rootless_path = ( - anatomy.find_root_template_from_path(path) - ) - if success: - path = rootless_path - else: - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(path)) - return path - - def get_files_info(self, instance, integrated_file_sizes): - """ Prepare 'files' portion for attached resources and main asset. - Combining records from 'transfers' and 'hardlinks' parts from - instance. - All attached resources should be added, currently without - Context info. - - Arguments: - instance: the current instance being published - integrated_file_sizes: dictionary of destination path (absolute) - and its file size - Returns: - output_resources: array of dictionaries to be added to 'files' key - in representation - """ - resources = list(instance.data.get("transfers", [])) - resources.extend(list(instance.data.get("hardlinks", []))) - - self.log.debug("get_resource_files_info.resources:{}". - format(resources)) - - output_resources = [] - anatomy = instance.context.data["anatomy"] - for _src, dest in resources: - path = self.get_rootless_path(anatomy, dest) - dest = self.get_dest_temp_url(dest) - file_hash = source_hash(dest) - if self.TMP_FILE_EXT and \ - ',{}'.format(self.TMP_FILE_EXT) in file_hash: - file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), - '') - - file_info = self.prepare_file_info(path, - integrated_file_sizes[dest], - file_hash, - instance=instance) - output_resources.append(file_info) - - return output_resources - - def get_dest_temp_url(self, dest): - """ Enhance destination path with TMP_FILE_EXT to denote temporary - file. - Temporary files will be renamed after successful registration - into DB and full copy to destination - - Arguments: - dest: destination url of published file (absolute) - Returns: - dest: destination path + '.TMP_FILE_EXT' - """ - if self.TMP_FILE_EXT and '.{}'.format(self.TMP_FILE_EXT) not in dest: - dest += '.{}'.format(self.TMP_FILE_EXT) - return dest - - def prepare_file_info(self, path, size=None, file_hash=None, - sites=None, instance=None): - """ Prepare information for one file (asset or resource) - - Arguments: - path: destination url of published file (rootless) - size(optional): size of file in bytes - file_hash(optional): hash of file for synchronization validation - sites(optional): array of published locations, - [ {'name':'studio', 'created_dt':date} by default - keys expected ['studio', 'site1', 'gdrive1'] - instance(dict, optional): to get collected settings - Returns: - rec: dictionary with filled info - """ - local_site = 'studio' # default - remote_site = None - always_accesible = [] - sync_project_presets = None - - rec = { - "_id": ObjectId(), - "path": path - } - if size: - rec["size"] = size - - if file_hash: - rec["hash"] = file_hash - - if sites: - rec["sites"] = sites - else: - system_sync_server_presets = ( - instance.context.data["system_settings"] - ["modules"] - ["sync_server"]) - log.debug("system_sett:: {}".format(system_sync_server_presets)) - - if system_sync_server_presets["enabled"]: - sync_project_presets = ( - instance.context.data["project_settings"] - ["global"] - ["sync_server"]) - - if sync_project_presets and sync_project_presets["enabled"]: - local_site, remote_site = self._get_sites(sync_project_presets) - - always_accesible = sync_project_presets["config"]. \ - get("always_accessible_on", []) - - already_attached_sites = {} - meta = {"name": local_site, "created_dt": datetime.now()} - rec["sites"] = [meta] - already_attached_sites[meta["name"]] = meta["created_dt"] - - if sync_project_presets and sync_project_presets["enabled"]: - if remote_site and \ - remote_site not in already_attached_sites.keys(): - # add remote - meta = {"name": remote_site.strip()} - rec["sites"].append(meta) - already_attached_sites[meta["name"]] = None - - # add alternative sites - rec, already_attached_sites = self._add_alternative_sites( - system_sync_server_presets, already_attached_sites, rec) - - # add skeleton for site where it should be always synced to - for always_on_site in set(always_accesible): - if always_on_site not in already_attached_sites.keys(): - meta = {"name": always_on_site.strip()} - rec["sites"].append(meta) - already_attached_sites[meta["name"]] = None - - log.debug("final sites:: {}".format(rec["sites"])) - - return rec - - def _get_sites(self, sync_project_presets): - """Returns tuple (local_site, remote_site)""" - local_site_id = get_local_site_id() - local_site = sync_project_presets["config"]. \ - get("active_site", "studio").strip() - - if local_site == 'local': - local_site = local_site_id - - remote_site = sync_project_presets["config"].get("remote_site") - - if remote_site == 'local': - remote_site = local_site_id - - return local_site, remote_site - - def _add_alternative_sites(self, - system_sync_server_presets, - already_attached_sites, - rec): - """Loop through all configured sites and add alternatives. - - See SyncServerModule.handle_alternate_site - """ - conf_sites = system_sync_server_presets.get("sites", {}) - - alt_site_pairs = self._get_alt_site_pairs(conf_sites) - - already_attached_keys = list(already_attached_sites.keys()) - for added_site in already_attached_keys: - real_created = already_attached_sites[added_site] - for alt_site in alt_site_pairs.get(added_site, []): - if alt_site in already_attached_sites.keys(): - continue - meta = {"name": alt_site} - # alt site inherits state of 'created_dt' - if real_created: - meta["created_dt"] = real_created - rec["sites"].append(meta) - already_attached_sites[meta["name"]] = real_created - - return rec, already_attached_sites - - def _get_alt_site_pairs(self, conf_sites): - """Returns dict of site and its alternative sites. - - If `site` has alternative site, it means that alt_site has 'site' as - alternative site - Args: - conf_sites (dict) - Returns: - (dict): {'site': [alternative sites]...} - """ - alt_site_pairs = defaultdict(list) - for site_name, site_info in conf_sites.items(): - alt_sites = set(site_info.get("alternative_sites", [])) - alt_site_pairs[site_name].extend(alt_sites) - - for alt_site in alt_sites: - alt_site_pairs[alt_site].append(site_name) - - for site_name, alt_sites in alt_site_pairs.items(): - sites_queue = deque(alt_sites) - while sites_queue: - alt_site = sites_queue.popleft() - - # safety against wrong config - # {"SFTP": {"alternative_site": "SFTP"} - if alt_site == site_name or alt_site not in alt_site_pairs: - continue - - for alt_alt_site in alt_site_pairs[alt_site]: - if ( - alt_alt_site != site_name - and alt_alt_site not in alt_sites - ): - alt_sites.append(alt_alt_site) - sites_queue.append(alt_alt_site) - - return alt_site_pairs - - def handle_destination_files(self, integrated_file_sizes, mode): - """ Clean destination files - Called when error happened during integrating to DB or to disk - OR called to rename uploaded files from temporary name to final to - highlight publishing in progress/broken - Used to clean unwanted files - - Arguments: - integrated_file_sizes: dictionary, file urls as keys, size as value - mode: 'remove' - clean files, - 'finalize' - rename files, - remove TMP_FILE_EXT suffix denoting temp file - """ - if integrated_file_sizes: - for file_url, _file_size in integrated_file_sizes.items(): - if not os.path.exists(file_url): - self.log.debug( - "File {} was not found.".format(file_url) - ) - continue - - try: - if mode == 'remove': - self.log.debug("Removing file {}".format(file_url)) - os.remove(file_url) - if mode == 'finalize': - new_name = re.sub( - r'\.{}$'.format(self.TMP_FILE_EXT), - '', - file_url - ) - - if os.path.exists(new_name): - self.log.debug( - "Overwriting file {} to {}".format( - file_url, new_name - ) - ) - shutil.copy(file_url, new_name) - os.remove(file_url) - else: - self.log.debug( - "Renaming file {} to {}".format( - file_url, new_name - ) - ) - os.rename(file_url, new_name) - except OSError: - self.log.error("Cannot {} file {}".format(mode, file_url), - exc_info=True) - six.reraise(*sys.exc_info()) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 0da1e0ea74..36e00858ed 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -365,9 +365,6 @@ } ] }, - "IntegrateAsset": { - "skip_host_families": [] - }, "IntegrateHeroVersion": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3164cfb62d..81a13d9c57 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -993,37 +993,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "IntegrateAsset", - "label": "Integrate Asset", - "is_group": true, - "children": [ - { - "type": "list", - "key": "skip_host_families", - "label": "Skip hosts and families", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "hosts-enum", - "key": "host", - "label": "Host" - }, - { - "type": "list", - "key": "families", - "label": "Families", - "object_type": "text" - } - ] - } - } - ] - }, { "type": "dict", "collapsible": true, From 20bb87bf7da6428a7a31690d4d74791736913026 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Apr 2023 10:02:12 +0200 Subject: [PATCH 212/446] AYON: Update settings conversion (#4837) * Change how settings are converted and updated with latest settings models * handle cases when shotgrid is using new addon * added some more project settings * updated system settings * fill missing keys from defaults --- openpype/settings/ayon_settings.py | 468 ++++++++++++++--------------- 1 file changed, 231 insertions(+), 237 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 0402a7b5f3..df6379c3e5 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -94,7 +94,9 @@ def _convert_applications_groups(groups, clear_metadata): variant_dynamic_labels = {} for variant in group.pop("variants"): variant_name = variant.pop("name") - variant_dynamic_labels[variant_name] = variant.pop("label") + label = variant.get("label") + if label and label != variant_name: + variant_dynamic_labels[variant_name] = label variant_envs = variant[environment_key] if isinstance(variant_envs, six.string_types): variant[environment_key] = json.loads(variant_envs) @@ -110,12 +112,20 @@ def _convert_applications_groups(groups, clear_metadata): return output -def _convert_applications(ayon_settings, output, clear_metadata): +def _convert_applications_system_settings( + ayon_settings, output, clear_metadata +): # Addon settings addon_settings = ayon_settings["applications"] + # Remove project settings + addon_settings.pop("only_available", None) + # Applications settings ayon_apps = addon_settings["applications"] + if "adsk_3dsmax" in ayon_apps: + ayon_apps["3dsmax"] = ayon_apps.pop("adsk_3dsmax") + additional_apps = ayon_apps.pop("additional_apps") applications = _convert_applications_groups( ayon_apps, clear_metadata @@ -133,40 +143,49 @@ def _convert_applications(ayon_settings, output, clear_metadata): output["tools"] = {"tool_groups": tools} -def _convert_general(ayon_settings, output): +def _convert_general(ayon_settings, output, default_settings): # TODO get studio name/code core_settings = ayon_settings["core"] environments = core_settings["environments"] if isinstance(environments, six.string_types): environments = json.loads(environments) - output["general"].update({ + general = default_settings["general"] + general.update({ "log_to_server": False, "studio_name": core_settings["studio_name"], "studio_code": core_settings["studio_code"], "environments": environments }) + output["general"] = general def _convert_kitsu_system_settings(ayon_settings, output): - kitsu_settings = output["modules"]["kitsu"] - kitsu_settings["server"] = ayon_settings["kitsu"]["server"] + output["modules"]["kitsu"] = { + "server": ayon_settings["kitsu"]["server"] + } def _convert_ftrack_system_settings(ayon_settings, output): - # TODO implement and convert rest of ftrack settings - ftrack_settings = output["modules"]["ftrack"] ayon_ftrack = ayon_settings["ftrack"] - ftrack_settings["ftrack_server"] = ayon_ftrack["ftrack_server"] + # Ignore if new ftrack addon is used + if "service_settings" in ayon_ftrack: + output["ftrack"] = ayon_ftrack + return + + output["modules"]["ftrack"] = { + "ftrack_server": ayon_ftrack["ftrack_server"] + } def _convert_shotgrid_system_settings(ayon_settings, output): ayon_shotgrid = ayon_settings["shotgrid"] # Skip conversion if different ayon addon is used if "leecher_manager_url" not in ayon_shotgrid: + output["shotgrid"] = ayon_shotgrid return - shotgrid_settings = output["modules"]["shotgrid"] + shotgrid_settings = {} for key in ( "leecher_manager_url", "leecher_backend_url", @@ -180,73 +199,94 @@ def _convert_shotgrid_system_settings(ayon_settings, output): new_items[name] = item shotgrid_settings["shotgrid_settings"] = new_items + output["modules"]["shotgrid"] = shotgrid_settings -def _convert_timers_manager(ayon_settings, output): - manager_settings = output["modules"]["timers_manager"] + +def _convert_timers_manager_system_settings(ayon_settings, output): ayon_manager = ayon_settings["timers_manager"] - for key in { - "auto_stop", "full_time", "message_time", "disregard_publishing" - }: - manager_settings[key] = ayon_manager[key] + manager_settings = { + key: ayon_manager[key] + for key in { + "auto_stop", "full_time", "message_time", "disregard_publishing" + } + } + output["modules"]["timers_manager"] = manager_settings -def _convert_clockify(ayon_settings, output): - clockify_settings = output["modules"]["clockify"] - ayon_clockify = ayon_settings["clockify"] - for key in { - "worskpace_name", - }: - clockify_settings[key] = ayon_clockify[key] +def _convert_clockify_system_settings(ayon_settings, output): + output["modules"]["clockify"] = ayon_settings["clockify"] -def _convert_deadline(ayon_settings, output): - deadline_settings = output["modules"]["deadline"] +def _convert_deadline_system_settings(ayon_settings, output): ayon_deadline = ayon_settings["deadline"] - deadline_urls = {} - for item in ayon_deadline["deadline_urls"]: - deadline_urls[item["name"]] = item["value"] - deadline_settings["deadline_urls"] = deadline_urls + deadline_settings = { + "deadline_urls": { + item["name"]: item["value"] + for item in ayon_deadline["deadline_urls"] + } + } + output["modules"]["deadline"] = deadline_settings -def _convert_muster(ayon_settings, output): - muster_settings = output["modules"]["muster"] +def _convert_muster_system_settings(ayon_settings, output): ayon_muster = ayon_settings["muster"] - templates_mapping = {} - for item in ayon_muster["templates_mapping"]: - templates_mapping[item["name"]] = item["value"] - muster_settings["templates_mapping"] = templates_mapping - muster_settings["MUSTER_REST_URL"] = ayon_muster["MUSTER_REST_URL"] - - -def _convert_royalrender(ayon_settings, output): - royalrender_settings = output["modules"]["royalrender"] - ayon_royalrender = ayon_settings["royalrender"] - royalrender_settings["rr_paths"] = { + templates_mapping = { item["name"]: item["value"] - for item in ayon_royalrender["rr_paths"] + for item in ayon_muster["templates_mapping"] + } + output["modules"]["muster"] = { + "templates_mapping": templates_mapping, + "MUSTER_REST_URL": ayon_muster["MUSTER_REST_URL"] } -def _convert_modules(ayon_settings, output, addon_versions): +def _convert_royalrender_system_settings(ayon_settings, output): + ayon_royalrender = ayon_settings["royalrender"] + output["modules"]["royalrender"] = { + "rr_paths": { + item["name"]: item["value"] + for item in ayon_royalrender["rr_paths"] + } + } + + +def _convert_modules_system( + ayon_settings, output, addon_versions, default_settings +): + # TODO remove when not needed + # - these modules are not and won't be in AYON avaialble + for module_name in ( + "addon_paths", + "avalon", + "job_queue", + "log_viewer", + "project_manager", + ): + output["modules"][module_name] = ( + default_settings["modules"][module_name] + ) + # TODO add all modules # TODO add 'enabled' values for key, func in ( ("kitsu", _convert_kitsu_system_settings), ("ftrack", _convert_ftrack_system_settings), ("shotgrid", _convert_shotgrid_system_settings), - ("timers_manager", _convert_timers_manager), - ("clockify", _convert_clockify), - ("deadline", _convert_deadline), - ("muster", _convert_muster), - ("royalrender", _convert_royalrender), + ("timers_manager", _convert_timers_manager_system_settings), + ("clockify", _convert_clockify_system_settings), + ("deadline", _convert_deadline_system_settings), + ("muster", _convert_muster_system_settings), + ("royalrender", _convert_royalrender_system_settings), ): if key in ayon_settings: func(ayon_settings, output) - for module_name, value in output["modules"].items(): - if "enabled" not in value: + output_modules = output["modules"] + for module_name, value in default_settings["modules"].items(): + if "enabled" not in value or module_name not in output_modules: continue - value["enabled"] = module_name in addon_versions + + output_modules[module_name]["enabled"] = module_name in addon_versions # Missing modules conversions # - "sync_server" -> renamed to sitesync @@ -255,60 +295,61 @@ def _convert_modules(ayon_settings, output, addon_versions): def convert_system_settings(ayon_settings, default_settings, addon_versions): - output = copy.deepcopy(default_settings) + default_settings = copy.deepcopy(default_settings) + output = { + "modules": {} + } if "applications" in ayon_settings: - _convert_applications(ayon_settings, output, False) + _convert_applications_system_settings(ayon_settings, output, False) if "core" in ayon_settings: - _convert_general(ayon_settings, output) + _convert_general(ayon_settings, output, default_settings) - _convert_modules(ayon_settings, output, addon_versions) + _convert_modules_system( + ayon_settings, + output, + addon_versions, + default_settings + ) + for key, value in default_settings.items(): + if key not in output: + output[key] = value return output # --------- Project settings --------- +def _convert_applications_project_settings(ayon_settings, output): + if "applications" not in ayon_settings: + return + + output["applications"] = { + "only_available": ayon_settings["applications"]["only_available"] + } + + def _convert_blender_project_settings(ayon_settings, output): if "blender" not in ayon_settings: return ayon_blender = ayon_settings["blender"] - blender_settings = output["blender"] _convert_host_imageio(ayon_blender) - ayon_workfile_build = ayon_blender["workfile_builder"] - blender_workfile_build = blender_settings["workfile_builder"] - for key in ("create_first_version", "custom_templates"): - blender_workfile_build[key] = ayon_workfile_build[key] - ayon_publish = ayon_blender["publish"] - model_validators = ayon_publish.pop("model_validators", None) - if model_validators is not None: - for src_key, dst_key in ( - ("validate_mesh_has_uvs", "ValidateMeshHasUvs"), - ("validate_mesh_no_negative_scale", "ValidateMeshNoNegativeScale"), - ("validate_transform_zero", "ValidateTransformZero"), - ): - ayon_publish[dst_key] = model_validators.pop(src_key) - blender_publish = blender_settings["publish"] - for key in tuple(ayon_publish.keys()): - blender_publish[key] = ayon_publish[key] + for plugin in ("ExtractThumbnail", "ExtractPlayblast"): + plugin_settings = ayon_publish[plugin] + plugin_settings["presets"] = json.loads(plugin_settings["presets"]) + + output["blender"] = ayon_blender def _convert_celaction_project_settings(ayon_settings, output): if "celaction" not in ayon_settings: return - ayon_celaction_publish = ayon_settings["celaction"]["publish"] - celaction_publish_settings = output["celaction"]["publish"] - output["celaction"]["imageio"] = _convert_host_imageio( - ayon_celaction_publish - ) + ayon_celaction = ayon_settings["celaction"] + _convert_host_imageio(ayon_celaction) - for plugin_name in tuple(celaction_publish_settings.keys()): - if plugin_name in ayon_celaction_publish: - celaction_publish_settings[plugin_name] = ( - ayon_celaction_publish[plugin_name] - ) + output["celaction"] = ayon_celaction def _convert_flame_project_settings(ayon_settings, output): @@ -316,25 +357,8 @@ def _convert_flame_project_settings(ayon_settings, output): return ayon_flame = ayon_settings["flame"] - flame_settings = output["flame"] - flame_settings["create"] = ayon_flame["create"] - - ayon_load_flame = ayon_flame["load"] - load_flame_settings = flame_settings["load"] - # Wrong settings model on server side - for src_key, dst_key in ( - ("load_clip", "LoadClip"), - ("load_clip_batch", "LoadClipBatch"), - ): - if src_key in ayon_load_flame: - ayon_load_flame[dst_key] = ayon_load_flame.pop(src_key) - - for plugin_name in tuple(load_flame_settings.keys()): - if plugin_name in ayon_load_flame: - load_flame_settings[plugin_name] = ayon_load_flame[plugin_name] ayon_publish_flame = ayon_flame["publish"] - flame_publish_settings = flame_settings["publish"] # 'ExtractSubsetResources' changed model of 'export_presets_mapping' # - some keys were moved under 'other_parameters' ayon_subset_resources = ayon_publish_flame["ExtractSubsetResources"] @@ -347,11 +371,6 @@ def _convert_flame_project_settings(ayon_settings, output): new_subset_resources[name] = item ayon_subset_resources["export_presets_mapping"] = new_subset_resources - for plugin_name in tuple(flame_publish_settings.keys()): - if plugin_name in ayon_publish_flame: - flame_publish_settings[plugin_name] = ( - ayon_publish_flame[plugin_name] - ) # 'imageio' changed model # - missing subkey 'project' which is in root of 'imageio' model @@ -359,17 +378,21 @@ def _convert_flame_project_settings(ayon_settings, output): ayon_imageio_flame = ayon_flame["imageio"] if "project" not in ayon_imageio_flame: profile_mapping = ayon_imageio_flame.pop("profilesMapping") - ayon_imageio_flame = { + ayon_flame["imageio"] = { "project": ayon_imageio_flame, "profilesMapping": profile_mapping } - flame_settings["imageio"] = ayon_imageio_flame + + output["flame"] = ayon_flame def _convert_fusion_project_settings(ayon_settings, output): if "fusion" not in ayon_settings: return + ayon_fusion = ayon_settings["fusion"] + _convert_host_imageio(ayon_fusion) + ayon_imageio_fusion = ayon_fusion["imageio"] if "ocioSettings" in ayon_imageio_fusion: @@ -383,14 +406,18 @@ def _convert_fusion_project_settings(ayon_settings, output): ayon_ocio_setting["configFilePath"] = paths ayon_imageio_fusion["ocio"] = ayon_ocio_setting + else: + paths = ayon_imageio_fusion["ocio"].pop("configFilePath") + for key, value in tuple(paths.items()): + new_value = [] + if value: + new_value.append(value) + paths[key] = new_value + ayon_imageio_fusion["ocio"]["configFilePath"] = paths _convert_host_imageio(ayon_imageio_fusion) - imageio_fusion_settings = output["fusion"]["imageio"] - for key in ( - "imageio", - ): - imageio_fusion_settings[key] = ayon_fusion[key] + output["fusion"] = ayon_fusion def _convert_maya_project_settings(ayon_settings, output): @@ -398,7 +425,6 @@ def _convert_maya_project_settings(ayon_settings, output): return ayon_maya = ayon_settings["maya"] - openpype_maya = output["maya"] # Change key of render settings ayon_maya["RenderSettings"] = ayon_maya.pop("render_settings") @@ -474,6 +500,12 @@ def _convert_maya_project_settings(ayon_settings, output): for item in validate_rendern_settings[key] ] + plugin_path_attributes = ayon_publish["ValidatePluginPathAttributes"] + plugin_path_attributes["attribute"] = { + item["name"]: item["value"] + for item in plugin_path_attributes["attribute"] + } + ayon_capture_preset = ayon_publish["ExtractPlayblast"]["capture_preset"] display_options = ayon_capture_preset["DisplayOptions"] for key in ("background", "backgroundBottom", "backgroundTop"): @@ -486,6 +518,12 @@ def _convert_maya_project_settings(ayon_settings, output): ): ayon_capture_preset[dst_key] = ayon_capture_preset.pop(src_key) + viewport_options = ayon_capture_preset["Viewport Options"] + viewport_options["pluginObjects"] = { + item["name"]: item["value"] + for item in viewport_options["pluginObjects"] + } + # Extract Camera Alembic bake attributes try: bake_attributes = json.loads( @@ -509,33 +547,24 @@ def _convert_maya_project_settings(ayon_settings, output): _convert_host_imageio(ayon_maya) - same_keys = { - "imageio", - "scriptsmenu", - "templated_workfile_build", - "load", - "create", - "publish", - "mel_workspace", - "ext_mapping", - "workfile_build", - "filters", - "maya-dirmap", - "RenderSettings", - } - for key in same_keys: - openpype_maya[key] = ayon_maya[key] + output["maya"] = ayon_maya def _convert_nuke_knobs(knobs): new_knobs = [] for knob in knobs: knob_type = knob["type"] - value = knob[knob_type] if knob_type == "boolean": knob_type = "bool" + if knob_type != "bool": + value = knob[knob_type] + elif knob_type in knob: + value = knob[knob_type] + else: + value = knob["boolean"] + new_knob = { "type": knob_type, "name": knob["name"], @@ -569,7 +598,6 @@ def _convert_nuke_project_settings(ayon_settings, output): return ayon_nuke = ayon_settings["nuke"] - openpype_nuke = output["nuke"] # --- Dirmap --- dirmap = ayon_nuke.pop("dirmap") @@ -627,10 +655,15 @@ def _convert_nuke_project_settings(ayon_settings, output): new_review_data_outputs = {} for item in ayon_publish["ExtractReviewDataMov"]["outputs"]: - name = item.pop("name") item["reformat_node_config"] = _convert_nuke_knobs( item["reformat_node_config"]) + + for node in item["reformat_nodes_config"]["reposition_nodes"]: + node["knobs"] = _convert_nuke_knobs(node["knobs"]) + + name = item.pop("name") new_review_data_outputs[name] = item + ayon_publish["ExtractReviewDataMov"]["outputs"] = new_review_data_outputs # TODO 'ExtractThumbnail' does not have ideal schema in v3 @@ -659,18 +692,7 @@ def _convert_nuke_project_settings(ayon_settings, output): for item in ayon_imageio["nodes"]["overrideNodes"]: item["knobs"] = _convert_nuke_knobs(item["knobs"]) - # Store converted values to openpype values - for key in ( - "scriptsmenu", - "nuke-dirmap", - "filters", - "load", - "create", - "publish", - "workfile_builder", - "imageio", - ): - openpype_nuke[key] = ayon_nuke[key] + output["nuke"] = ayon_nuke def _convert_hiero_project_settings(ayon_settings, output): @@ -678,7 +700,7 @@ def _convert_hiero_project_settings(ayon_settings, output): return ayon_hiero = ayon_settings["hiero"] - openpype_hiero = output["hiero"] + _convert_host_imageio(ayon_hiero) new_gui_filters = {} for item in ayon_hiero.pop("filters"): @@ -689,17 +711,7 @@ def _convert_hiero_project_settings(ayon_settings, output): new_gui_filters[key] = subvalue ayon_hiero["filters"] = new_gui_filters - _convert_host_imageio(ayon_hiero) - - for key in ( - "create", - "filters", - "imageio", - "load", - "publish", - "scriptsmenu", - ): - openpype_hiero[key] = ayon_hiero[key] + output["hiero"] = ayon_hiero def _convert_photoshop_project_settings(ayon_settings, output): @@ -707,38 +719,22 @@ def _convert_photoshop_project_settings(ayon_settings, output): return ayon_photoshop = ayon_settings["photoshop"] - photoshop_settings = output["photoshop"] + _convert_host_imageio(ayon_photoshop) + collect_review = ayon_photoshop["publish"]["CollectReview"] if "active" in collect_review: collect_review["publish"] = collect_review.pop("active") - _convert_host_imageio(ayon_photoshop) - - for key in ( - "create", - "publish", - "workfile_builder", - "imageio", - ): - photoshop_settings[key] = ayon_photoshop[key] + output["photoshop"] = ayon_photoshop def _convert_tvpaint_project_settings(ayon_settings, output): if "tvpaint" not in ayon_settings: return ayon_tvpaint = ayon_settings["tvpaint"] - tvpaint_settings = output["tvpaint"] _convert_host_imageio(ayon_tvpaint) - for key in ( - "stop_timer_on_application_exit", - "load", - "workfile_builder", - "imageio", - ): - tvpaint_settings[key] = ayon_tvpaint[key] - filters = {} for item in ayon_tvpaint["filters"]: value = item["value"] @@ -748,15 +744,9 @@ def _convert_tvpaint_project_settings(ayon_settings, output): except ValueError: value = {} filters[item["name"]] = value - tvpaint_settings["filters"] = filters + ayon_tvpaint["filters"] = filters ayon_publish_settings = ayon_tvpaint["publish"] - tvpaint_publish_settings = tvpaint_settings["publish"] - for plugin_name in ("CollectRenderScene", "ExtractConvertToEXR"): - tvpaint_publish_settings[plugin_name] = ( - ayon_publish_settings[plugin_name] - ) - for plugin_name in ( "ValidateProjectSettings", "ValidateMarks", @@ -764,29 +754,28 @@ def _convert_tvpaint_project_settings(ayon_settings, output): "ValidateAssetName", ): ayon_value = ayon_publish_settings[plugin_name] - tvpaint_value = tvpaint_publish_settings[plugin_name] for src_key, dst_key in ( ("action_enabled", "optional"), ("action_enable", "active"), ): if src_key in ayon_value: - tvpaint_value[dst_key] = ayon_value[src_key] + ayon_value[dst_key] = ayon_value.pop(src_key) - review_color = ayon_publish_settings["ExtractSequence"]["review_bg"] - tvpaint_publish_settings["ExtractSequence"]["review_bg"] = _convert_color( - review_color + extract_sequence_setting = ayon_publish_settings["ExtractSequence"] + extract_sequence_setting["review_bg"] = _convert_color( + extract_sequence_setting["review_bg"] ) + output["tvpaint"] = ayon_tvpaint + def _convert_traypublisher_project_settings(ayon_settings, output): if "traypublisher" not in ayon_settings: return ayon_traypublisher = ayon_settings["traypublisher"] - traypublisher_settings = output["traypublisher"] _convert_host_imageio(ayon_traypublisher) - traypublisher_settings["imageio"] = ayon_traypublisher["imageio"] ayon_editorial_simple = ( ayon_traypublisher["editorial_creators"]["editorial_simple"] @@ -822,9 +811,7 @@ def _convert_traypublisher_project_settings(ayon_settings, output): } ayon_editorial_simple["shot_add_tasks"] = new_shot_add_tasks - traypublisher_settings["editorial_creators"][ - "editorial_simple" - ] = ayon_editorial_simple + output["traypublisher"] = ayon_traypublisher def _convert_webpublisher_project_settings(ayon_settings, output): @@ -841,8 +828,8 @@ def _convert_webpublisher_project_settings(ayon_settings, output): item["name"]: item["value"] for item in ayon_collect_files["task_type_to_family"] } - output["webpublisher"]["publish"] = ayon_publish - output["webpublisher"]["imageio"] = ayon_webpublisher["imageio"] + + output["webpublisher"] = ayon_webpublisher def _convert_deadline_project_settings(ayon_settings, output): @@ -850,7 +837,6 @@ def _convert_deadline_project_settings(ayon_settings, output): return ayon_deadline = ayon_settings["deadline"] - deadline_settings = output["deadline"] for key in ("deadline_urls",): ayon_deadline.pop(key) @@ -885,21 +871,26 @@ def _convert_deadline_project_settings(ayon_settings, output): item["name"]: item["value"] for item in process_subsetted_job.pop("aov_filter") } - deadline_publish_settings = deadline_settings["publish"] - for key in tuple(deadline_publish_settings.keys()): - if key in ayon_deadline_publish: - deadline_publish_settings[key] = ayon_deadline_publish[key] + + output["deadline"] = ayon_deadline + + +def _convert_royalrender_project_settings(ayon_settings, output): + if "royalrender" not in ayon_settings: + return + ayon_royalrender = ayon_settings["royalrender"] + output["royalrender"] = { + "publish": ayon_royalrender["publish"] + } def _convert_kitsu_project_settings(ayon_settings, output): if "kitsu" not in ayon_settings: return - ayon_kitsu = ayon_settings["kitsu"] - kitsu_settings = output["kitsu"] - for key in tuple(kitsu_settings.keys()): - if key in ayon_kitsu: - kitsu_settings[key] = ayon_kitsu[key] + ayon_kitsu_settings = ayon_settings["kitsu"] + ayon_kitsu_settings.pop("server") + output["kitsu"] = ayon_kitsu_settings def _convert_shotgrid_project_settings(ayon_settings, output): @@ -907,6 +898,10 @@ def _convert_shotgrid_project_settings(ayon_settings, output): return ayon_shotgrid = ayon_settings["shotgrid"] + # This means that a different variant of addon is used + if "leecher_backend_url" not in ayon_shotgrid: + return + for key in { "leecher_backend_url", "filter_projects_by_login", @@ -922,10 +917,7 @@ def _convert_shotgrid_project_settings(ayon_settings, output): if "task" in task_field: task_field["step"] = task_field.pop("task") - shotgrid_settings = output["shotgrid"] - for key in tuple(shotgrid_settings.keys()): - if key in ayon_shotgrid: - shotgrid_settings[key] = ayon_shotgrid[key] + output["shotgrid"] = ayon_settings["shotgrid"] def _convert_slack_project_settings(ayon_settings, output): @@ -933,23 +925,28 @@ def _convert_slack_project_settings(ayon_settings, output): return ayon_slack = ayon_settings["slack"] - slack_settings = output["slack"] ayon_slack.pop("enabled", None) for profile in ayon_slack["publish"]["CollectSlackFamilies"]["profiles"]: profile["tasks"] = profile.pop("task_names") profile["subsets"] = profile.pop("subset_names") - for key in tuple(slack_settings.keys()): - if key in ayon_settings: - slack_settings[key] = ayon_settings[key] + output["slack"] = ayon_slack -def _convert_global_project_settings(ayon_settings, output): +def _convert_global_project_settings(ayon_settings, output, default_settings): if "core" not in ayon_settings: return ayon_core = ayon_settings["core"] - global_settings = output["global"] + + _convert_host_imageio(ayon_core) + + for key in ( + "environments", + "studio_name", + "studio_code", + ): + ayon_core.pop(key) # Publish conversion ayon_publish = ayon_core["publish"] @@ -988,39 +985,26 @@ def _convert_global_project_settings(ayon_settings, output): for extract_burnin_def in extract_burnin_defs } - global_publish = global_settings["publish"] - ayon_integrate_hero = ayon_publish["IntegrateHeroVersion"] - global_integrate_hero = global_publish["IntegrateHeroVersion"] - for key, value in global_integrate_hero.items(): - if key not in ayon_integrate_hero: - ayon_integrate_hero[key] = value - ayon_cleanup = ayon_publish["CleanUp"] if "patterns" in ayon_cleanup: ayon_cleanup["paterns"] = ayon_cleanup.pop("patterns") - for key in tuple(global_publish.keys()): - if key in ayon_publish: - global_publish[key] = ayon_publish[key] - # Project root settings - for json_key in ("project_folder_structure", "project_environments"): - try: - value = json.loads(ayon_core[json_key]) - except ValueError: - value = {} - global_publish[json_key] = value + ayon_core["project_environments"] = json.loads( + ayon_core["project_environments"] + ) + ayon_core["project_folder_structure"] = json.dumps(json.loads( + ayon_core["project_folder_structure"] + )) # Tools settings ayon_tools = ayon_core["tools"] - global_tools = global_settings["tools"] ayon_create_tool = ayon_tools["creator"] new_smart_select_families = { item["name"]: item["task_names"] for item in ayon_create_tool["families_smart_select"] } ayon_create_tool["families_smart_select"] = new_smart_select_families - global_tools["creator"] = ayon_create_tool ayon_loader_tool = ayon_tools["loader"] for profile in ayon_loader_tool["family_filter_profiles"]: @@ -1028,15 +1012,18 @@ def _convert_global_project_settings(ayon_settings, output): profile["filter_families"] = ( profile.pop("template_publish_families") ) - global_tools["loader"] = ayon_loader_tool - global_tools["publish"] = ayon_tools["publish"] + ayon_core["sync_server"] = ( + default_settings["global"]["sync_server"] + ) + output["global"] = ayon_core def convert_project_settings(ayon_settings, default_settings): # Missing settings # - standalonepublisher - output = copy.deepcopy(default_settings) + default_settings = copy.deepcopy(default_settings) + output = {} exact_match = { "aftereffects", "harmony", @@ -1047,7 +1034,9 @@ def convert_project_settings(ayon_settings, default_settings): for key in exact_match: if key in ayon_settings: output[key] = ayon_settings[key] + _convert_host_imageio(output[key]) + _convert_applications_project_settings(ayon_settings, output) _convert_blender_project_settings(ayon_settings, output) _convert_celaction_project_settings(ayon_settings, output) _convert_flame_project_settings(ayon_settings, output) @@ -1061,11 +1050,16 @@ def convert_project_settings(ayon_settings, default_settings): _convert_webpublisher_project_settings(ayon_settings, output) _convert_deadline_project_settings(ayon_settings, output) + _convert_royalrender_project_settings(ayon_settings, output) _convert_kitsu_project_settings(ayon_settings, output) _convert_shotgrid_project_settings(ayon_settings, output) _convert_slack_project_settings(ayon_settings, output) - _convert_global_project_settings(ayon_settings, output) + _convert_global_project_settings(ayon_settings, output, default_settings) + + for key, value in default_settings.items(): + if key not in output: + output[key] = value return output From 899f9965e4a121a097b671264577d088d652a4a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 14 Apr 2023 16:30:50 +0200 Subject: [PATCH 213/446] AYON: Small fixes (#4841) * us constants from ayon api for environments * updated ayon api * define 'BUILTIN_OCIO_ROOT' in ayon start script * fox missing modules settings * use window open on QApplication exec * fix graphql queries * implemented 'get_archived_assets' --- ayon_start.py | 22 +- common/ayon_common/connection/credentials.py | 19 +- .../ayon_common/connection/ui/login_window.py | 54 +- openpype/client/server/entities.py | 17 +- openpype/client/server/openpype_comp.py | 4 +- openpype/client/server/operations.py | 4 +- openpype/settings/ayon_settings.py | 18 +- .../vendor/python/common/ayon_api/__init__.py | 54 +- .../vendor/python/common/ayon_api/_api.py | 276 ++++- .../python/common/ayon_api/constants.py | 7 +- .../python/common/ayon_api/entity_hub.py | 156 ++- .../vendor/python/common/ayon_api/events.py | 2 +- .../python/common/ayon_api/exceptions.py | 12 +- .../vendor/python/common/ayon_api/graphql.py | 151 ++- .../python/common/ayon_api/graphql_queries.py | 76 +- .../python/common/ayon_api/operations.py | 5 +- .../python/common/ayon_api/server_api.py | 1011 ++++++++++++++--- .../vendor/python/common/ayon_api/version.py | 2 +- 18 files changed, 1550 insertions(+), 340 deletions(-) diff --git a/ayon_start.py b/ayon_start.py index 11677b4415..e45fbf4680 100644 --- a/ayon_start.py +++ b/ayon_start.py @@ -74,7 +74,6 @@ if "--headless" in sys.argv: elif os.getenv("OPENPYPE_HEADLESS_MODE") != "1": os.environ.pop("OPENPYPE_HEADLESS_MODE", None) - IS_BUILT_APPLICATION = getattr(sys, "frozen", False) HEADLESS_MODE_ENABLED = os.environ.get("OPENPYPE_HEADLESS_MODE") == "1" SILENT_MODE_ENABLED = any(arg in _silent_commands for arg in sys.argv) @@ -137,6 +136,14 @@ os.environ["OPENPYPE_REPOS_ROOT"] = AYON_ROOT os.environ["AVALON_LABEL"] = "AYON" # Set name of pyblish UI import os.environ["PYBLISH_GUI"] = "pyblish_pype" +# Set builtin OCIO root +os.environ["BUILTIN_OCIO_ROOT"] = os.path.join( + AYON_ROOT, + "vendor", + "bin", + "ocioconfig", + "OpenColorIOConfigs" +) import blessed # noqa: E402 import certifi # noqa: E402 @@ -183,6 +190,7 @@ if not os.getenv("SSL_CERT_FILE"): elif os.getenv("SSL_CERT_FILE") != certifi.where(): _print("--- your system is set to use custom CA certificate bundle.") +from ayon_api.constants import SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY from ayon_common.connection.credentials import ( ask_to_login_ui, add_server, @@ -252,12 +260,12 @@ def _connect_to_ayon_server(): if HEADLESS_MODE_ENABLED: _print("!!! Cannot open v4 Login dialog in headless mode.") _print(( - "!!! Please use `AYON_SERVER_URL` to specify server address" - " and 'AYON_TOKEN' to specify user's token." - )) + "!!! Please use `{}` to specify server address" + " and '{}' to specify user's token." + ).format(SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY)) sys.exit(1) - current_url = os.environ.get("AYON_SERVER_URL") + current_url = os.environ.get(SERVER_URL_ENV_KEY) url, token, username = ask_to_login_ui(current_url, always_on_top=True) if url is not None and token is not None: confirm_server_login(url, token, username) @@ -345,10 +353,10 @@ def boot(): t.echo(i) try: - cli.main(obj={}, prog_name="openpype") + cli.main(obj={}, prog_name="ayon") except Exception: # noqa exc_info = sys.exc_info() - _print("!!! OpenPype crashed:") + _print("!!! AYON crashed:") traceback.print_exception(*exc_info) sys.exit(1) diff --git a/common/ayon_common/connection/credentials.py b/common/ayon_common/connection/credentials.py index 4d1a97ee00..23cac9a8fc 100644 --- a/common/ayon_common/connection/credentials.py +++ b/common/ayon_common/connection/credentials.py @@ -17,6 +17,7 @@ from typing import Optional, Union, Any import ayon_api +from ayon_api.constants import SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY from ayon_api.exceptions import UrlError from ayon_api.utils import ( validate_url, @@ -383,7 +384,7 @@ def load_environments(): """Load environments on startup. Handle environments needed for connection with server. Environments are - 'AYON_SERVER_URL' and 'AYON_TOKEN'. + 'AYON_SERVER_URL' and 'AYON_API_KEY'. Server is looked up from environment. Already set environent is not changed. If environemnt is not filled then last server stored in appdirs @@ -394,16 +395,16 @@ def load_environments(): based on server url. """ - server_url = os.environ.get("AYON_SERVER_URL") + server_url = os.environ.get(SERVER_URL_ENV_KEY) if not server_url: server_url = get_last_server() if not server_url: return - os.environ["AYON_SERVER_URL"] = server_url + os.environ[SERVER_URL_ENV_KEY] = server_url - if not os.environ.get("AYON_TOKEN"): + if not os.environ.get(SERVER_API_ENV_KEY): if token := load_token(server_url): - os.environ["AYON_TOKEN"] = token + os.environ[SERVER_API_ENV_KEY] = token def set_environments(url: str, token: str): @@ -441,7 +442,7 @@ def need_server_or_login() -> bool: bool: 'True' if server and token are available. Otherwise 'False'. """ - server_url = os.environ.get("AYON_SERVER_URL") + server_url = os.environ.get(SERVER_URL_ENV_KEY) if not server_url: return True @@ -450,12 +451,14 @@ def need_server_or_login() -> bool: except UrlError: return True - token = os.environ.get("AYON_TOKEN") + token = os.environ.get(SERVER_API_ENV_KEY) if token: return not is_token_valid(server_url, token) token = load_token(server_url) - return not is_token_valid(server_url, token) + if token: + return not is_token_valid(server_url, token) + return True def confirm_server_login(url, token, username): diff --git a/common/ayon_common/connection/ui/login_window.py b/common/ayon_common/connection/ui/login_window.py index d7c0558eec..566dc4f71f 100644 --- a/common/ayon_common/connection/ui/login_window.py +++ b/common/ayon_common/connection/ui/login_window.py @@ -674,29 +674,14 @@ def ask_to_login(url=None, username=None, always_on_top=False): if username: window.set_username(username) - _output = {"out": None} - - def _exec_window(): - window.exec_() - result = window.result() - out_url, out_token, out_username, _logged_out = result - _output["out"] = out_url, out_token, out_username - return _output["out"] - - # Use QTimer to exec dialog if application is not running yet - # - it is not possible to call 'exec_' on dialog without running app - # - it is but the window is stuck if not app_instance.startingUp(): - return _exec_window() - - timer = QtCore.QTimer() - timer.setSingleShot(True) - timer.timeout.connect(_exec_window) - timer.start() - # This can become main Qt loop. Maybe should live elsewhere - app_instance.exec_() - - return _output["out"] + window.exec_() + else: + window.open() + app_instance.exec_() + result = window.result() + out_url, out_token, out_username, _ = result + return out_url, out_token, out_username def change_user(url, username, api_key, always_on_top=False): @@ -735,23 +720,10 @@ def change_user(url, username, api_key, always_on_top=False): ) window.set_logged_in(True, url, username, api_key) - _output = {"out": None} - - def _exec_window(): - window.exec_() - _output["out"] = window.result() - return _output["out"] - - # Use QTimer to exec dialog if application is not running yet - # - it is not possible to call 'exec_' on dialog without running app - # - it is but the window is stuck if not app_instance.startingUp(): - return _exec_window() - - timer = QtCore.QTimer() - timer.setSingleShot(True) - timer.timeout.connect(_exec_window) - timer.start() - # This can become main Qt loop. Maybe should live elsewhere - app_instance.exec_() - return _output["out"] + window.exec_() + else: + window.open() + # This can become main Qt loop. Maybe should live elsewhere + app_instance.exec_() + return window.result() diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 5dc8af9a6d..b49c8dd505 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -216,8 +216,21 @@ def get_assets( yield convert_v4_folder_to_v3(folder, project_name) -def get_archived_assets(*args, **kwargs): - raise NotImplementedError("'get_archived_assets' not implemented") +def get_archived_assets( + project_name, + asset_ids=None, + asset_names=None, + parent_ids=None, + fields=None +): + return get_assets( + project_name, + asset_ids, + asset_names, + parent_ids, + True, + fields + ) def get_asset_ids_with_subsets(project_name, asset_ids=None): diff --git a/openpype/client/server/openpype_comp.py b/openpype/client/server/openpype_comp.py index 00ee0aae92..df3ffcc0d3 100644 --- a/openpype/client/server/openpype_comp.py +++ b/openpype/client/server/openpype_comp.py @@ -16,7 +16,7 @@ def folders_tasks_graphql_query(fields): project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - folders_field = project_field.add_field("folders", has_edges=True) + folders_field = project_field.add_field_with_edges("folders") folders_field.set_filter("ids", folder_ids_var) folders_field.set_filter("parentIds", parent_folder_ids_var) folders_field.set_filter("names", folder_names_var) @@ -25,7 +25,7 @@ def folders_tasks_graphql_query(fields): fields = set(fields) fields.discard("tasks") - tasks_field = folders_field.add_field("tasks", has_edges=True) + tasks_field = folders_field.add_field_with_edges("tasks") tasks_field.add_field("name") tasks_field.add_field("taskType") diff --git a/openpype/client/server/operations.py b/openpype/client/server/operations.py index 6148f6a098..0456680737 100644 --- a/openpype/client/server/operations.py +++ b/openpype/client/server/operations.py @@ -857,7 +857,7 @@ def delete_project(project_name, con=None): return con.delete_project(project_name) -def create_thumbnail(project_name, src_filepath, con=None): +def create_thumbnail(project_name, src_filepath, thumbnail_id=None, con=None): if con is None: con = get_server_api_connection() - return con.create_thumbnail(project_name, src_filepath) + return con.create_thumbnail(project_name, src_filepath, thumbnail_id) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index df6379c3e5..30efd61fe4 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -253,19 +253,6 @@ def _convert_royalrender_system_settings(ayon_settings, output): def _convert_modules_system( ayon_settings, output, addon_versions, default_settings ): - # TODO remove when not needed - # - these modules are not and won't be in AYON avaialble - for module_name in ( - "addon_paths", - "avalon", - "job_queue", - "log_viewer", - "project_manager", - ): - output["modules"][module_name] = ( - default_settings["modules"][module_name] - ) - # TODO add all modules # TODO add 'enabled' values for key, func in ( @@ -282,6 +269,11 @@ def _convert_modules_system( func(ayon_settings, output) output_modules = output["modules"] + # TODO remove when not needed + for module_name, value in default_settings["modules"].items(): + if module_name not in output_modules: + output_modules[module_name] = value + for module_name, value in default_settings["modules"].items(): if "enabled" not in value or module_name not in output_modules: continue diff --git a/openpype/vendor/python/common/ayon_api/__init__.py b/openpype/vendor/python/common/ayon_api/__init__.py index 700c1b3687..ee6672dd38 100644 --- a/openpype/vendor/python/common/ayon_api/__init__.py +++ b/openpype/vendor/python/common/ayon_api/__init__.py @@ -56,6 +56,7 @@ from ._api import ( query_graphql, get_addons_info, + get_addon_url, download_addon_private_file, get_dependencies_info, @@ -121,11 +122,35 @@ from ._api import ( get_representation_parents, get_repre_ids_by_context_filters, - create_thumbnail, get_thumbnail, get_folder_thumbnail, get_version_thumbnail, get_workfile_thumbnail, + create_thumbnail, + update_thumbnail, + + get_full_link_type_name, + get_link_types, + get_link_type, + create_link_type, + delete_link_type, + make_sure_link_type_exists, + + create_link, + delete_link, + get_entities_links, + get_folder_links, + get_folders_links, + get_task_links, + get_tasks_links, + get_subset_links, + get_subsets_links, + get_version_links, + get_versions_links, + get_representations_links, + get_representation_links, + + send_batch_operations, ) @@ -184,6 +209,7 @@ __all__ = ( "query_graphql", "get_addons_info", + "get_addon_url", "download_addon_private_file", "get_dependencies_info", @@ -248,9 +274,33 @@ __all__ = ( "get_representation_parents", "get_repre_ids_by_context_filters", - "create_thumbnail", "get_thumbnail", "get_folder_thumbnail", "get_version_thumbnail", "get_workfile_thumbnail", + "create_thumbnail", + "update_thumbnail", + + "get_full_link_type_name", + "get_link_types", + "get_link_type", + "create_link_type", + "delete_link_type", + "make_sure_link_type_exists", + + "create_link", + "delete_link", + "get_entities_links", + "get_folder_links", + "get_folders_links", + "get_task_links", + "get_tasks_links", + "get_subset_links", + "get_subsets_links", + "get_version_links", + "get_versions_links", + "get_representations_links", + "get_representation_links", + + "send_batch_operations", ) diff --git a/openpype/vendor/python/common/ayon_api/_api.py b/openpype/vendor/python/common/ayon_api/_api.py index 6410b459eb..ed730841ae 100644 --- a/openpype/vendor/python/common/ayon_api/_api.py +++ b/openpype/vendor/python/common/ayon_api/_api.py @@ -11,7 +11,7 @@ import socket from .constants import ( SERVER_URL_ENV_KEY, - SERVER_TOKEN_ENV_KEY, + SERVER_API_ENV_KEY, ) from .server_api import ServerAPI from .exceptions import FailedServiceInit @@ -21,7 +21,7 @@ class GlobalServerAPI(ServerAPI): """Extended server api which also handles storing tokens and url. Created object expect to have set environment variables - 'AYON_SERVER_URL'. Also is expecting filled 'AYON_TOKEN' + 'AYON_SERVER_URL'. Also is expecting filled 'AYON_API_KEY' but that can be filled afterwards with calling 'login' method. """ @@ -44,7 +44,7 @@ class GlobalServerAPI(ServerAPI): previous_token = self._access_token super(GlobalServerAPI, self).login(username, password) if self.has_valid_token and previous_token != self._access_token: - os.environ[SERVER_TOKEN_ENV_KEY] = self._access_token + os.environ[SERVER_API_ENV_KEY] = self._access_token @staticmethod def get_url(): @@ -52,7 +52,7 @@ class GlobalServerAPI(ServerAPI): @staticmethod def get_token(): - return os.environ.get(SERVER_TOKEN_ENV_KEY) + return os.environ.get(SERVER_API_ENV_KEY) @staticmethod def set_environments(url, token): @@ -64,7 +64,7 @@ class GlobalServerAPI(ServerAPI): """ os.environ[SERVER_URL_ENV_KEY] = url or "" - os.environ[SERVER_TOKEN_ENV_KEY] = token or "" + os.environ[SERVER_API_ENV_KEY] = token or "" class GlobalContext: @@ -151,7 +151,7 @@ class ServiceContext: connect=True ): token = cls.get_value_from_envs( - ("AY_API_KEY", "AYON_TOKEN"), + ("AY_API_KEY", "AYON_API_KEY"), token ) server_url = cls.get_value_from_envs( @@ -322,7 +322,7 @@ def get_server_api_connection(): """Access to global scope object of GlobalServerAPI. This access expect to have set environment variables 'AYON_SERVER_URL' - and 'AYON_TOKEN'. + and 'AYON_API_KEY'. Returns: GlobalServerAPI: Object of connection to server. @@ -521,6 +521,11 @@ def get_addons_info(*args, **kwargs): return con.get_addons_info(*args, **kwargs) +def get_addon_url(addon_name, addon_version, *subpaths): + con = get_server_api_connection() + return con.get_addon_url(addon_name, addon_version, *subpaths) + + def download_addon_private_file(*args, **kwargs): con = get_server_api_connection() return con.download_addon_private_file(*args, **kwargs) @@ -776,11 +781,6 @@ def delete_project(project_name): return con.delete_project(project_name) -def create_thumbnail(project_name, src_filepath): - con = get_server_api_connection() - return con.create_thumbnail(project_name, src_filepath) - - 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) @@ -801,11 +801,259 @@ def get_workfile_thumbnail(project_name, workfile_id, thumbnail_id=None): return con.get_workfile_thumbnail(project_name, workfile_id, thumbnail_id) -def create_thumbnail(project_name, src_filepath): +def create_thumbnail(project_name, src_filepath, thumbnail_id=None): con = get_server_api_connection() - return con.create_thumbnail(project_name, src_filepath) + return con.create_thumbnail(project_name, src_filepath, thumbnail_id) + + +def update_thumbnail(project_name, thumbnail_id, src_filepath): + con = get_server_api_connection() + return con.update_thumbnail(project_name, thumbnail_id, src_filepath) def get_default_fields_for_type(entity_type): con = get_server_api_connection() return con.get_default_fields_for_type(entity_type) + + +def get_full_link_type_name(link_type_name, input_type, output_type): + con = get_server_api_connection() + return con.get_full_link_type_name( + link_type_name, input_type, output_type) + + +def get_link_types(project_name): + con = get_server_api_connection() + return con.get_link_types(project_name) + + +def get_link_type(project_name, link_type_name, input_type, output_type): + con = get_server_api_connection() + return con.get_link_type( + project_name, link_type_name, input_type, output_type) + + +def create_link_type( + project_name, link_type_name, input_type, output_type, data=None): + con = get_server_api_connection() + return con.create_link_type( + project_name, link_type_name, input_type, output_type, data=data) + + +def delete_link_type(project_name, link_type_name, input_type, output_type): + con = get_server_api_connection() + return con.delete_link_type( + project_name, link_type_name, input_type, output_type) + + +def make_sure_link_type_exists( + project_name, link_type_name, input_type, output_type, data=None +): + con = get_server_api_connection() + return con.make_sure_link_type_exists( + project_name, link_type_name, input_type, output_type, data=data + ) + + +def create_link( + project_name, + link_type_name, + input_id, + input_type, + output_id, + output_type +): + con = get_server_api_connection() + return con.create_link( + project_name, + link_type_name, + input_id, input_type, + output_id, output_type + ) + + +def delete_link(project_name, link_id): + con = get_server_api_connection() + return con.delete_link(project_name, link_id) + + +def get_entities_links( + project_name, + entity_type, + entity_ids=None, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_entities_links( + project_name, + entity_type, + entity_ids, + link_types, + link_direction + ) + + +def get_folders_links( + project_name, + folder_ids=None, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_folders_links( + project_name, + folder_ids, + link_types, + link_direction + ) + + +def get_folder_links( + project_name, + folder_id, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_folder_links( + project_name, + folder_id, + link_types, + link_direction + ) + + +def get_tasks_links( + project_name, + task_ids=None, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_tasks_links( + project_name, + task_ids, + link_types, + link_direction + ) + + +def get_task_links( + project_name, + task_id, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_task_links( + project_name, + task_id, + link_types, + link_direction + ) + + +def get_subsets_links( + project_name, + subset_ids=None, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_subsets_links( + project_name, + subset_ids, + link_types, + link_direction + ) + + +def get_subset_links( + project_name, + subset_id, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_subset_links( + project_name, + subset_id, + link_types, + link_direction + ) + + +def get_versions_links( + project_name, + version_ids=None, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_versions_links( + project_name, + version_ids, + link_types, + link_direction + ) + + +def get_version_links( + project_name, + version_id, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_version_links( + project_name, + version_id, + link_types, + link_direction + ) + + +def get_representations_links( + project_name, + representation_ids=None, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_representations_links( + project_name, + representation_ids, + link_types, + link_direction + ) + + +def get_representation_links( + project_name, + representation_id, + link_types=None, + link_direction=None +): + con = get_server_api_connection() + return con.get_representation_links( + project_name, + representation_id, + link_types, + link_direction + ) + + +def send_batch_operations( + project_name, + operations, + can_fail=False, + raise_on_fail=True +): + con = get_server_api_connection() + return con.send_batch_operations( + project_name, + operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail + ) diff --git a/openpype/vendor/python/common/ayon_api/constants.py b/openpype/vendor/python/common/ayon_api/constants.py index e431af6f9d..03451756a0 100644 --- a/openpype/vendor/python/common/ayon_api/constants.py +++ b/openpype/vendor/python/common/ayon_api/constants.py @@ -1,5 +1,8 @@ +# Environments where server url and api key are stored for global connection SERVER_URL_ENV_KEY = "AYON_SERVER_URL" -SERVER_TOKEN_ENV_KEY = "AYON_TOKEN" +SERVER_API_ENV_KEY = "AYON_API_KEY" +# Backwards compatibility +SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY # --- Project --- DEFAULT_PROJECT_FIELDS = { @@ -102,4 +105,4 @@ DEFAULT_EVENT_FIELDS = { "topic", "updatedAt", "user", -} \ No newline at end of file +} diff --git a/openpype/vendor/python/common/ayon_api/entity_hub.py b/openpype/vendor/python/common/ayon_api/entity_hub.py index 76703d2e15..36489b6439 100644 --- a/openpype/vendor/python/common/ayon_api/entity_hub.py +++ b/openpype/vendor/python/common/ayon_api/entity_hub.py @@ -1,6 +1,6 @@ import copy import collections -from abc import ABCMeta, abstractmethod, abstractproperty +from abc import ABCMeta, abstractmethod import six from ._api import get_server_api_connection @@ -52,17 +52,35 @@ class EntityHub(object): @property def allow_data_changes(self): - """Entity hub allows changes of 'data' key on entities.""" + """Entity hub allows changes of 'data' key on entities. + + Data are private and not all users may have access to them. Also to get + 'data' for entity is required to use REST api calls, which means to + query each entity on-by-one from server. + + Returns: + bool: Data changes are allowed. + """ return self._allow_data_changes @property def project_name(self): + """Project name which is maintained by hub. + + Returns: + str: Name of project. + """ + return self._project_name @property def project_entity(self): - """Project entity.""" + """Project entity. + + Returns: + ProjectEntity: Project entity. + """ if self._project_entity is UNKNOWN_VALUE: self.fill_project_from_server() @@ -187,6 +205,12 @@ class EntityHub(object): @property def entities(self): + """Iterator over available entities. + + Returns: + Iterator[BaseEntity]: All queried/created entities cached in hub. + """ + for entity in self._entities_by_id.values(): yield entity @@ -194,8 +218,21 @@ class EntityHub(object): """Create folder object and add it to entity hub. Args: - parent (Union[ProjectEntity, FolderEntity]): Parent of added - folder. + folder_type (str): Type of folder. Folder type must be available in + config of project folder types. + entity_id (Union[str, None]): Id of the entity. New id is created if + not passed. + parent_id (Union[str, None]): Id of parent entity. + name (str): Name of entity. + label (Optional[str]): Folder label. + path (Optional[str]): Folder path. Path consist of all parent names + with slash('/') used as separator. + attribs (Dict[str, Any]): Attribute values. + data (Dict[str, Any]): Entity data (custom data). + thumbnail_id (Union[str, None]): Id of entity's thumbnail. + active (bool): Is entity active. + created (Optional[bool]): Entity is new. When 'None' is passed the + value is defined based on value of 'entity_id'. Returns: FolderEntity: Added folder entity. @@ -208,6 +245,27 @@ class EntityHub(object): return folder_entity def add_new_task(self, *args, created=True, **kwargs): + """Create folder object and add it to entity hub. + + Args: + task_type (str): Type of task. Task type must be available in + config of project folder types. + entity_id (Union[str, None]): Id of the entity. New id is created if + not passed. + parent_id (Union[str, None]): Id of parent entity. + name (str): Name of entity. + label (Optional[str]): Folder label. + attribs (Dict[str, Any]): Attribute values. + data (Dict[str, Any]): Entity data (custom data). + thumbnail_id (Union[str, None]): Id of entity's thumbnail. + active (bool): Is entity active. + created (Optional[bool]): Entity is new. When 'None' is passed the + value is defined based on value of 'entity_id'. + + Returns: + TaskEntity: Added task entity. + """ + task_entity = TaskEntity( *args, **kwargs, created=created, entity_hub=self ) @@ -428,7 +486,7 @@ class EntityHub(object): if parent_id is None: return - parent = self._entities_by_parent_id.get(parent_id) + parent = self._entities_by_id.get(parent_id) if parent is not None: parent.remove_child(entity.id) @@ -459,10 +517,12 @@ class EntityHub(object): reset_queue.append(child.id) def fill_project_from_server(self): - """Query project from server and create it's entity. + """Query project data from server and create project entity. + + This method will invalidate previous object of Project entity. Returns: - ProjectEntity: Entity that was created based on queried data. + ProjectEntity: Entity that was updated with server data. Raises: ValueError: When project was not found on server. @@ -844,17 +904,17 @@ class BaseEntity(object): entity are set as "current data" on server. Args: + entity_id (Union[str, None]): Id of the entity. New id is created if + not passed. + parent_id (Union[str, None]): Id of parent entity. name (str): Name of entity. attribs (Dict[str, Any]): Attribute values. data (Dict[str, Any]): Entity data (custom data). - parent_id (Union[str, None]): Id of parent entity. - entity_id (Union[str, None]): Id of the entity. New id is created if - not passed. thumbnail_id (Union[str, None]): Id of entity's thumbnail. active (bool): Is entity active. entity_hub (EntityHub): Object of entity hub which created object of the entity. - created (Union[bool, None]): Entity is new. When 'None' is passed the + created (Optional[bool]): Entity is new. When 'None' is passed the value is defined based on value of 'entity_id'. """ @@ -981,7 +1041,8 @@ class BaseEntity(object): return self._entity_hub.project_name - @abstractproperty + @property + @abstractmethod def entity_type(self): """Entity type coresponding to server. @@ -991,7 +1052,8 @@ class BaseEntity(object): pass - @abstractproperty + @property + @abstractmethod def parent_entity_types(self): """Entity type coresponding to server. @@ -1001,7 +1063,8 @@ class BaseEntity(object): pass - @abstractproperty + @property + @abstractmethod def changes(self): """Receive entity changes. @@ -1331,6 +1394,27 @@ class BaseEntity(object): class ProjectEntity(BaseEntity): + """Entity representing project on AYON server. + + Args: + project_code (str): Project code. + library (bool): Is project library project. + folder_types (list[dict[str, Any]]): Folder types definition. + task_types (list[dict[str, Any]]): Task types definition. + entity_id (Optional[str]): Id of the entity. New id is created if + not passed. + parent_id (Union[str, None]): Id of parent entity. + name (str): Name of entity. + attribs (Dict[str, Any]): Attribute values. + data (Dict[str, Any]): Entity data (custom data). + thumbnail_id (Union[str, None]): Id of entity's thumbnail. + active (bool): Is entity active. + entity_hub (EntityHub): Object of entity hub which created object of + the entity. + created (Optional[bool]): Entity is new. When 'None' is passed the + value is defined based on value of 'entity_id'. + """ + entity_type = "project" parent_entity_types = [] # TODO These are hardcoded but maybe should be used from server??? @@ -1433,6 +1517,28 @@ class ProjectEntity(BaseEntity): class FolderEntity(BaseEntity): + """Entity representing a folder on AYON server. + + Args: + folder_type (str): Type of folder. Folder type must be available in + config of project folder types. + entity_id (Union[str, None]): Id of the entity. New id is created if + not passed. + parent_id (Union[str, None]): Id of parent entity. + name (str): Name of entity. + attribs (Dict[str, Any]): Attribute values. + data (Dict[str, Any]): Entity data (custom data). + thumbnail_id (Union[str, None]): Id of entity's thumbnail. + active (bool): Is entity active. + label (Optional[str]): Folder label. + path (Optional[str]): Folder path. Path consist of all parent names + with slash('/') used as separator. + entity_hub (EntityHub): Object of entity hub which created object of + the entity. + created (Optional[bool]): Entity is new. When 'None' is passed the + value is defined based on value of 'entity_id'. + """ + entity_type = "folder" parent_entity_types = ["folder", "project"] @@ -1587,6 +1693,26 @@ class FolderEntity(BaseEntity): class TaskEntity(BaseEntity): + """Entity representing a task on AYON server. + + Args: + task_type (str): Type of task. Task type must be available in config + of project task types. + entity_id (Union[str, None]): Id of the entity. New id is created if + not passed. + parent_id (Union[str, None]): Id of parent entity. + name (str): Name of entity. + label (Optional[str]): Task label. + attribs (Dict[str, Any]): Attribute values. + data (Dict[str, Any]): Entity data (custom data). + thumbnail_id (Union[str, None]): Id of entity's thumbnail. + active (bool): Is entity active. + entity_hub (EntityHub): Object of entity hub which created object of + the entity. + created (Optional[bool]): Entity is new. When 'None' is passed the + value is defined based on value of 'entity_id'. + """ + entity_type = "task" parent_entity_types = ["folder"] diff --git a/openpype/vendor/python/common/ayon_api/events.py b/openpype/vendor/python/common/ayon_api/events.py index 1ea9331244..aa256f6cfc 100644 --- a/openpype/vendor/python/common/ayon_api/events.py +++ b/openpype/vendor/python/common/ayon_api/events.py @@ -49,4 +49,4 @@ class ServerEvent(object): "payload": self.payload, "finished": self.finished, "store": self.store - } \ No newline at end of file + } diff --git a/openpype/vendor/python/common/ayon_api/exceptions.py b/openpype/vendor/python/common/ayon_api/exceptions.py index 0ff09770b5..db4917e90a 100644 --- a/openpype/vendor/python/common/ayon_api/exceptions.py +++ b/openpype/vendor/python/common/ayon_api/exceptions.py @@ -33,6 +33,16 @@ class ServerNotReached(ServerError): pass +class RequestError(Exception): + def __init__(self, message, response): + self.response = response + super(RequestError, self).__init__(message) + + +class HTTPRequestError(RequestError): + pass + + class GraphQlQueryFailed(Exception): def __init__(self, errors, query, variables): if variables is None: @@ -94,4 +104,4 @@ class FailedOperations(Exception): class FailedServiceInit(Exception): - pass \ No newline at end of file + pass diff --git a/openpype/vendor/python/common/ayon_api/graphql.py b/openpype/vendor/python/common/ayon_api/graphql.py index 93349e9608..854f207a00 100644 --- a/openpype/vendor/python/common/ayon_api/graphql.py +++ b/openpype/vendor/python/common/ayon_api/graphql.py @@ -1,6 +1,6 @@ import copy import numbers -from abc import ABCMeta, abstractproperty, abstractmethod +from abc import ABCMeta, abstractmethod import six @@ -232,21 +232,31 @@ class GraphQlQuery: self._children.append(field) field.set_parent(self) - def add_field(self, name, has_edges=None): + def add_field_with_edges(self, name): + """Add field with edges to query. + + Args: + name (str): Field name e.g. 'tasks'. + + Returns: + GraphQlQueryEdgeField: Created field object. + """ + + item = GraphQlQueryEdgeField(name, self) + self.add_obj_field(item) + return item + + def add_field(self, name): """Add field to query. Args: name (str): Field name e.g. 'id'. - has_edges (bool): Field has edges so it need paging. Returns: - BaseGraphQlQueryField: Created field object. + GraphQlQueryField: Created field object. """ - if has_edges: - item = GraphQlQueryEdgeField(name, self) - else: - item = GraphQlQueryField(name, self) + item = GraphQlQueryField(name, self) self.add_obj_field(item) return item @@ -376,7 +386,6 @@ class BaseGraphQlQueryField(object): name (str): Name of field. parent (Union[BaseGraphQlQueryField, GraphQlQuery]): Parent object of a field. - has_edges (bool): Field has edges and should handle paging. """ def __init__(self, name, parent): @@ -401,6 +410,36 @@ class BaseGraphQlQueryField(object): def __repr__(self): return "<{} {}>".format(self.__class__.__name__, self.path) + def add_variable(self, key, value_type, value=None): + """Add variable to query. + + Args: + key (str): Variable name. + value_type (str): Type of expected value in variables. This is + graphql type e.g. "[String!]", "Int", "Boolean", etc. + value (Any): Default value for variable. Can be changed later. + + Returns: + QueryVariable: Created variable object. + + Raises: + KeyError: If variable was already added before. + """ + + return self._parent.add_variable(key, value_type, value) + + def get_variable(self, key): + """Variable object. + + Args: + key (str): Variable name added to headers. + + Returns: + QueryVariable: Variable object used in query string. + """ + + return self._parent.get_variable(key) + @property def need_query(self): """Still need query from server. @@ -414,11 +453,21 @@ class BaseGraphQlQueryField(object): if self._need_query: return True - for child in self._children: + for child in self._children_iter(): if child.need_query: return True return False + def _children_iter(self): + """Iterate over all children fields of object. + + Returns: + Iterator[BaseGraphQlQueryField]: Children fields. + """ + + for child in self._children: + yield child + def sum_edge_fields(self, max_limit=None): """Check how many edge fields query has. @@ -437,7 +486,7 @@ class BaseGraphQlQueryField(object): if isinstance(self, GraphQlQueryEdgeField): counter = 1 - for child in self._children: + for child in self._children_iter(): counter += child.sum_edge_fields(max_limit) if max_limit is not None and counter >= max_limit: break @@ -451,7 +500,8 @@ class BaseGraphQlQueryField(object): def indent(self): return self._parent.child_indent + self.offset - @abstractproperty + @property + @abstractmethod def child_indent(self): pass @@ -459,13 +509,14 @@ class BaseGraphQlQueryField(object): def query_item(self): return self._query_item - @abstractproperty + @property + @abstractmethod def has_edges(self): pass @property def child_has_edges(self): - for child in self._children: + for child in self._children_iter(): if child.has_edges or child.child_has_edges: return True return False @@ -487,7 +538,7 @@ class BaseGraphQlQueryField(object): return self._path def reset_cursor(self): - for child in self._children: + for child in self._children_iter(): child.reset_cursor() def get_variable_value(self, *args, **kwargs): @@ -518,11 +569,13 @@ class BaseGraphQlQueryField(object): self._children.append(field) field.set_parent(self) - def add_field(self, name, has_edges=None): - if has_edges: - item = GraphQlQueryEdgeField(name, self) - else: - item = GraphQlQueryField(name, self) + def add_field_with_edges(self, name): + item = GraphQlQueryEdgeField(name, self) + self.add_obj_field(item) + return item + + def add_field(self, name): + item = GraphQlQueryField(name, self) self.add_obj_field(item) return item @@ -580,7 +633,7 @@ class BaseGraphQlQueryField(object): def _fake_children_parse(self): """Mark children as they don't need query.""" - for child in self._children: + for child in self._children_iter(): child.parse_result({}, {}, {}) @abstractmethod @@ -673,12 +726,38 @@ class GraphQlQueryEdgeField(BaseGraphQlQueryField): def __init__(self, *args, **kwargs): super(GraphQlQueryEdgeField, self).__init__(*args, **kwargs) self._cursor = None + self._edge_children = [] @property def child_indent(self): offset = self.offset * 2 return self.indent + offset + def _children_iter(self): + for child in super(GraphQlQueryEdgeField, self)._children_iter(): + yield child + + for child in self._edge_children: + yield child + + def add_obj_field(self, field): + if field in self._edge_children: + return + + super(GraphQlQueryEdgeField, self).add_obj_field(field) + + def add_obj_edge_field(self, field): + if field in self._edge_children or field in self._children: + return + + self._edge_children.append(field) + field.set_parent(self) + + def add_edge_field(self, name): + item = GraphQlQueryField(name, self) + self.add_obj_edge_field(item) + return item + def reset_cursor(self): # Reset cursor only for edges self._cursor = None @@ -733,6 +812,9 @@ class GraphQlQueryEdgeField(BaseGraphQlQueryField): nodes_by_cursor[edge_cursor] = edge_value node_values.append(edge_value) + for child in self._edge_children: + child.parse_result(edge, edge_value, progress_data) + for child in self._children: child.parse_result(edge["node"], edge_value, progress_data) @@ -740,12 +822,12 @@ class GraphQlQueryEdgeField(BaseGraphQlQueryField): return change_cursor = True - for child in self._children: + for child in self._children_iter(): if child.need_query: change_cursor = False if change_cursor: - for child in self._children: + for child in self._children_iter(): child.reset_cursor() self._cursor = new_cursor @@ -761,7 +843,7 @@ class GraphQlQueryEdgeField(BaseGraphQlQueryField): return filters def calculate_query(self): - if not self._children: + if not self._children and not self._edge_children: raise ValueError("Missing child definitions for edges {}".format( self.path )) @@ -779,16 +861,21 @@ class GraphQlQueryEdgeField(BaseGraphQlQueryField): edges_offset = offset + self.offset * " " node_offset = edges_offset + self.offset * " " output.append(edges_offset + "edges {") - output.append(node_offset + "node {") + for field in self._edge_children: + output.append(field.calculate_query()) - for field in self._children: - output.append( - field.calculate_query() - ) + if self._children: + output.append(node_offset + "node {") + + for field in self._children: + output.append( + field.calculate_query() + ) + + output.append(node_offset + "}") + if self.child_has_edges: + output.append(node_offset + "cursor") - output.append(node_offset + "}") - if self.child_has_edges: - output.append(node_offset + "cursor") output.append(edges_offset + "}") # Add page information diff --git a/openpype/vendor/python/common/ayon_api/graphql_queries.py b/openpype/vendor/python/common/ayon_api/graphql_queries.py index b6d5c5fcb3..4df377ea18 100644 --- a/openpype/vendor/python/common/ayon_api/graphql_queries.py +++ b/openpype/vendor/python/common/ayon_api/graphql_queries.py @@ -25,6 +25,58 @@ def fields_to_dict(fields): return output +def add_links_fields(entity_field, nested_fields): + if "links" not in nested_fields: + return + links_fields = nested_fields.pop("links") + + link_edge_fields = { + "id", + "linkType", + "projectName", + "entityType", + "entityId", + "direction", + "description", + "author", + } + if isinstance(links_fields, dict): + simple_fields = set(links_fields) + simple_variant = len(simple_fields - link_edge_fields) == 0 + else: + simple_variant = True + simple_fields = link_edge_fields + + link_field = entity_field.add_field_with_edges("links") + + link_type_var = link_field.add_variable("linkTypes", "[String!]") + link_dir_var = link_field.add_variable("linkDirection", "String!") + link_field.set_filter("linkTypes", link_type_var) + link_field.set_filter("direction", link_dir_var) + + if simple_variant: + for key in simple_fields: + link_field.add_edge_field(key) + return + + query_queue = collections.deque() + for key, value in links_fields.items(): + if key in link_edge_fields: + link_field.add_edge_field(key) + continue + query_queue.append((key, value, link_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)) + + def project_graphql_query(fields): query = GraphQlQuery("ProjectQuery") project_name_var = query.add_variable("projectName", "String!") @@ -51,7 +103,7 @@ def project_graphql_query(fields): def projects_graphql_query(fields): query = GraphQlQuery("ProjectsQuery") - projects_field = query.add_field("projects", has_edges=True) + projects_field = query.add_field_with_edges("projects") nested_fields = fields_to_dict(fields) @@ -83,7 +135,7 @@ def folders_graphql_query(fields): project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - folders_field = project_field.add_field("folders", has_edges=True) + folders_field = project_field.add_field_with_edges("folders") folders_field.set_filter("ids", folder_ids_var) folders_field.set_filter("parentIds", parent_folder_ids_var) folders_field.set_filter("names", folder_names_var) @@ -91,6 +143,7 @@ def folders_graphql_query(fields): folders_field.set_filter("hasSubsets", has_subsets_var) nested_fields = fields_to_dict(fields) + add_links_fields(folders_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -119,7 +172,7 @@ def tasks_graphql_query(fields): project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - tasks_field = project_field.add_field("tasks", has_edges=True) + tasks_field = project_field.add_field_with_edges("tasks") tasks_field.set_filter("ids", task_ids_var) # WARNING: At moment when this been created 'names' filter is not supported tasks_field.set_filter("names", task_names_var) @@ -127,6 +180,7 @@ def tasks_graphql_query(fields): tasks_field.set_filter("folderIds", folder_ids_var) nested_fields = fields_to_dict(fields) + add_links_fields(tasks_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -155,12 +209,13 @@ def subsets_graphql_query(fields): project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - subsets_field = project_field.add_field("subsets", has_edges=True) + subsets_field = project_field.add_field_with_edges("subsets") subsets_field.set_filter("ids", subset_ids_var) subsets_field.set_filter("names", subset_names_var) subsets_field.set_filter("folderIds", folder_ids_var) nested_fields = fields_to_dict(set(fields)) + add_links_fields(subsets_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -194,7 +249,7 @@ def versions_graphql_query(fields): project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - subsets_field = project_field.add_field("versions", has_edges=True) + subsets_field = project_field.add_field_with_edges("versions") subsets_field.set_filter("ids", version_ids_var) subsets_field.set_filter("subsetIds", subset_ids_var) subsets_field.set_filter("versions", versions_var) @@ -203,6 +258,7 @@ def versions_graphql_query(fields): subsets_field.set_filter("heroOrLatestOnly", hero_or_latest_only_var) nested_fields = fields_to_dict(set(fields)) + add_links_fields(subsets_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -231,12 +287,13 @@ def representations_graphql_query(fields): project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - repres_field = project_field.add_field("representations", has_edges=True) + repres_field = project_field.add_field_with_edges("representations") repres_field.set_filter("ids", repre_ids_var) repres_field.set_filter("versionIds", version_ids_var) repres_field.set_filter("names", repre_names_var) nested_fields = fields_to_dict(set(fields)) + add_links_fields(repres_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -266,7 +323,7 @@ def representations_parents_qraphql_query( project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - repres_field = project_field.add_field("representations", has_edges=True) + repres_field = project_field.add_field_with_edges("representations") repres_field.add_field("id") repres_field.set_filter("ids", repre_ids_var) version_field = repres_field.add_field("version") @@ -306,12 +363,13 @@ def workfiles_info_graphql_query(fields): project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - workfiles_field = project_field.add_field("workfiles", has_edges=True) + workfiles_field = project_field.add_field_with_edges("workfiles") workfiles_field.set_filter("ids", workfiles_info_ids) workfiles_field.set_filter("taskIds", task_ids_var) workfiles_field.set_filter("paths", paths_var) nested_fields = fields_to_dict(set(fields)) + add_links_fields(workfiles_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -337,7 +395,7 @@ def events_graphql_query(fields): users_var = query.add_variable("eventUsers", "[String!]") include_logs_var = query.add_variable("includeLogsFilter", "Boolean!") - events_field = query.add_field("events", has_edges=True) + events_field = query.add_field_with_edges("events") events_field.set_filter("topics", topics_var) events_field.set_filter("projects", projects_var) events_field.set_filter("states", states_var) diff --git a/openpype/vendor/python/common/ayon_api/operations.py b/openpype/vendor/python/common/ayon_api/operations.py index 21adc229d2..b5689de7c0 100644 --- a/openpype/vendor/python/common/ayon_api/operations.py +++ b/openpype/vendor/python/common/ayon_api/operations.py @@ -1,7 +1,7 @@ import copy import collections import uuid -from abc import ABCMeta, abstractproperty +from abc import ABCMeta, abstractmethod import six @@ -301,7 +301,8 @@ class AbstractOperation(object): def entity_type(self): return self._entity_type - @abstractproperty + @property + @abstractmethod def operation_name(self): """Stringified type of operation.""" diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index e3a42e4dad..8d52b484d8 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -46,6 +46,7 @@ from .exceptions import ( AuthenticationError, ServerNotReached, ServerError, + HTTPRequestError, ) from .utils import ( RepresentationParents, @@ -134,7 +135,10 @@ class RestApiResponse(object): return self.status def raise_for_status(self): - self._response.raise_for_status() + try: + self._response.raise_for_status() + except requests.exceptions.HTTPError as exc: + raise HTTPRequestError(str(exc), exc.response) def __enter__(self, *args, **kwargs): return self._response.__enter__(*args, **kwargs) @@ -143,9 +147,7 @@ class RestApiResponse(object): return key in self.data def __repr__(self): - return "<{}: {} ({})>".format( - self.__class__.__name__, self.status, self.detail - ) + return "<{} [{}]>".format(self.__class__.__name__, self.status) def __len__(self): return 200 <= self.status < 400 @@ -297,6 +299,14 @@ class ServerAPI(object): 'production'). """ + _entity_types_link_mapping = { + "folder": ("folderIds", "folders"), + "task": ("taskIds", "tasks"), + "subset": ("subsetIds", "subsets"), + "version": ("versionIds", "versions"), + "representation": ("representationIds", "representations"), + } + def __init__( self, base_url, @@ -1465,6 +1475,35 @@ class ServerAPI(object): response.raise_for_status() return response.data + def get_addon_url(self, addon_name, addon_version, *subpaths): + """Calculate url to addon route. + + Example: + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'https://your.url.com/addons/example/1.0.0/private/my.zip' + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + subpaths (tuple[str]): Any amount of subpaths that are added to + addon url. + + Returns: + str: Final url. + """ + + ending = "" + if subpaths: + ending = "/{}".format("/".join(subpaths)) + return "{}/addons/{}/{}{}".format( + self._base_url, + addon_name, + addon_version, + ending + ) + def download_addon_private_file( self, addon_name, @@ -1503,10 +1542,10 @@ class ServerAPI(object): if not os.path.exists(dst_dirpath): os.makedirs(dst_dirpath) - url = "{}/addons/{}/{}/private/{}".format( - self._base_url, + url = self.get_addon_url( addon_name, addon_version, + "private", filename ) self.download_file( @@ -1779,9 +1818,13 @@ class ServerAPI(object): dict[str, Any]: Schema of studio/project settings. """ - endpoint = "addons/{}/{}/schema".format(addon_name, addon_version) + args = tuple() if project_name: - endpoint += "/{}".format(project_name) + args = (project_name, ) + + endpoint = self.get_addon_url( + addon_name, addon_version, "schema", *args + ) result = self.get(endpoint) result.raise_for_status() return result.data @@ -2407,6 +2450,146 @@ class ServerAPI(object): fill_own_attribs(folder) yield folder + def get_folder_by_id( + self, + project_name, + folder_id, + fields=None, + own_attributes=False + ): + """Query folder entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Folder entity data or None if was not found. + """ + + folders = self.get_folders( + project_name, + folder_ids=[folder_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_by_path( + self, + project_name, + folder_path, + fields=None, + own_attributes=False + ): + """Query folder entity by path. + + Folder path is a path to folder with all parent names joined by slash. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_path (str): Folder path. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Folder entity data or None if was not found. + """ + + folders = self.get_folders( + project_name, + folder_paths=[folder_path], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_by_name( + self, + project_name, + folder_name, + fields=None, + own_attributes=False + ): + """Query folder entity by path. + + Warnings: + Folder name is not a unique identifier of a folder. Function is + kept for OpenPype 3 compatibility. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_name (str): Folder name. + fields (Union[Iterable[str], None]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (bool): Attribute values that are not explicitly set + on entity will have 'None' value. + + Returns: + Union[dict, None]: Folder entity data or None if was not found. + """ + + folders = self.get_folders( + project_name, + folder_names=[folder_name], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_ids_with_subsets(self, project_name, folder_ids=None): + """Find folders which have at least one subset. + + Folders that have at least one subset should be immutable, so they + should not change path -> change of name or name of any parent + is not possible. + + Args: + project_name (str): Name of project. + folder_ids (Union[Iterable[str], None]): Limit folder ids filtering + to a set of folders. If set to None all folders on project are + checked. + + Returns: + set[str]: Folder ids that have at least one subset. + """ + + if folder_ids is not None: + folder_ids = set(folder_ids) + if not folder_ids: + return set() + + query = folders_graphql_query({"id"}) + query.set_variable_value("projectName", project_name) + query.set_variable_value("folderHasSubsets", True) + if folder_ids: + query.set_variable_value("folderIds", list(folder_ids)) + + parsed_data = query.query(self) + folders = parsed_data["project"]["folders"] + return { + folder["id"] + for folder in folders + } + def get_tasks( self, project_name, @@ -2569,147 +2752,6 @@ class ServerAPI(object): return task return None - - def get_folder_by_id( - self, - project_name, - folder_id, - fields=None, - own_attributes=False - ): - """Query folder entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_id (str): Folder id. - fields (Union[Iterable[str], None]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. - - Returns: - Union[dict, None]: Folder entity data or None if was not found. - """ - - folders = self.get_folders( - project_name, - folder_ids=[folder_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for folder in folders: - return folder - return None - - def get_folder_by_path( - self, - project_name, - folder_path, - fields=None, - own_attributes=False - ): - """Query folder entity by path. - - Folder path is a path to folder with all parent names joined by slash. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_path (str): Folder path. - fields (Union[Iterable[str], None]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. - - Returns: - Union[dict, None]: Folder entity data or None if was not found. - """ - - folders = self.get_folders( - project_name, - folder_paths=[folder_path], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for folder in folders: - return folder - return None - - def get_folder_by_name( - self, - project_name, - folder_name, - fields=None, - own_attributes=False - ): - """Query folder entity by path. - - Warnings: - Folder name is not a unique identifier of a folder. Function is - kept for OpenPype 3 compatibility. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_name (str): Folder name. - fields (Union[Iterable[str], None]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. - - Returns: - Union[dict, None]: Folder entity data or None if was not found. - """ - - folders = self.get_folders( - project_name, - folder_names=[folder_name], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for folder in folders: - return folder - return None - - def get_folder_ids_with_subsets(self, project_name, folder_ids=None): - """Find folders which have at least one subset. - - Folders that have at least one subset should be immutable, so they - should not change path -> change of name or name of any parent - is not possible. - - Args: - project_name (str): Name of project. - folder_ids (Union[Iterable[str], None]): Limit folder ids filtering - to a set of folders. If set to None all folders on project are - checked. - - Returns: - set[str]: Folder ids that have at least one subset. - """ - - if folder_ids is not None: - folder_ids = set(folder_ids) - if not folder_ids: - return set() - - query = folders_graphql_query({"id"}) - query.set_variable_value("projectName", project_name) - query.set_variable_value("folderHasSubsets", True) - if folder_ids: - query.set_variable_value("folderIds", list(folder_ids)) - - parsed_data = query.query(self) - folders = parsed_data["project"]["folders"] - return { - folder["id"] - for folder in folders - } - def _filter_subset( self, project_name, subset, active, own_attributes, use_rest ): @@ -4021,6 +4063,97 @@ class ServerAPI(object): project_name, "workfile", workfile_id, thumbnail_id ) + def _get_thumbnail_mime_type(self, thumbnail_path): + """Get thumbnail mime type on thumbnail creation based on source path. + + Args: + thumbnail_path (str): Path to thumbnail source fie. + + Returns: + str: Mime type used for thumbnail creation. + + Raises: + ValueError: Mime type cannot be determined. + """ + + ext = os.path.splitext(thumbnail_path)[-1].lower() + if ext == ".png": + return "image/png" + + elif ext in (".jpeg", ".jpg"): + return "image/jpeg" + + raise ValueError( + "Thumbnail source file has unknown extensions {}".format(ext)) + + def create_thumbnail(self, project_name, src_filepath, thumbnail_id=None): + """Create new thumbnail on server from passed path. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + src_filepath (str): Filepath to thumbnail which should be uploaded. + thumbnail_id (str): Prepared if of thumbnail. + + Returns: + str: Created thumbnail id. + + Raises: + ValueError: When thumbnail source cannot be processed. + """ + + if not os.path.exists(src_filepath): + raise ValueError("Entered filepath does not exist.") + + if thumbnail_id: + self.update_thumbnail( + project_name, + thumbnail_id, + src_filepath + ) + return thumbnail_id + + mime_type = self._get_thumbnail_mime_type(src_filepath) + with open(src_filepath, "rb") as stream: + content = stream.read() + + response = self.raw_post( + "projects/{}/thumbnails".format(project_name), + headers={"Content-Type": mime_type}, + data=content + ) + response.raise_for_status() + return response.data["id"] + + def update_thumbnail(self, project_name, thumbnail_id, src_filepath): + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + Raises: + ValueError: When thumbnail source cannot be processed. + """ + + if not os.path.exists(src_filepath): + raise ValueError("Entered filepath does not exist.") + + mime_type = self._get_thumbnail_mime_type(src_filepath) + with open(src_filepath, "rb") as stream: + content = stream.read() + + response = self.raw_put( + "projects/{}/thumbnails/{}".format(project_name, thumbnail_id), + headers={"Content-Type": mime_type}, + data=content + ) + response.raise_for_status() + def create_project( self, project_name, @@ -4046,7 +4179,6 @@ class ServerAPI(object): library_project (bool): Project is library project. preset_name (str): Name of anatomy preset. Default is used if not passed. - con (ServerAPI): Connection to server with logged user. Raises: ValueError: When project name already exists. @@ -4107,55 +4239,562 @@ class ServerAPI(object): ) ) - def create_thumbnail(self, project_name, src_filepath): - """Create new thumbnail on server from passed path. + # --- Links --- + def get_full_link_type_name(self, link_type_name, input_type, output_type): + """Calculate full link type name used for query from server. Args: - project_name (str): Project where the thumbnail will be created - and can be used. - src_filepath (str): Filepath to thumbnail which should be uploaded. + link_type_name (str): Type of link. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. Returns: - str: Created thumbnail id. - - Todos: - Define more specific exceptions for thumbnail creation. - - Raises: - ValueError: When thumbnail creation fails (due to many reasons). + str: Full name of link type used for query from server. """ - if not os.path.exists(src_filepath): - raise ValueError("Entered filepath does not exist.") + return "|".join([link_type_name, input_type, output_type]) - ext = os.path.splitext(src_filepath)[-1].lower() - if ext == ".png": - mime_type = "image/png" + def get_link_types(self, project_name): + """All link types available on a project. - elif ext in (".jpeg", ".jpg"): - mime_type = "image/jpeg" + Example output: + [ + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } + ] - else: - raise ValueError( - "Thumbnail source file has unknown extensions {}".format(ext)) + Args: + project_name (str): Name of project where to look for link types. - with open(src_filepath, "rb") as stream: - content = stream.read() + Returns: + list[dict[str, Any]]: Link types available on project. + """ - response = self.raw_post( - "projects/{}/thumbnails".format(project_name), - headers={"Content-Type": mime_type}, - data=content + response = self.get("projects/{}/links/types".format(project_name)) + response.raise_for_status() + return response.data["types"] + + def get_link_type( + self, project_name, link_type_name, input_type, output_type + ): + """Get link type data. + + There is not dedicated REST endpoint to get single link type, + so method 'get_link_types' is used. + + Example output: + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } + + Args: + project_name (str): Project where link type is available. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + + Returns: + Union[None, dict[str, Any]]: Link type information. + """ + + full_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type ) - if response.status_code != 200: - _detail = response.data.get("detail") - details = "" - if _detail: - details = " {}".format(_detail) - raise ValueError( - "Failed to create thumbnail.{}".format(details)) - return response.data["id"] + for link_type in self.get_link_types(project_name): + if link_type["name"] == full_type_name: + return link_type + return None + def create_link_type( + self, project_name, link_type_name, input_type, output_type, data=None + ): + """Create or update link type on server. + + Warning: + Because PUT is used for creation it is also used for update. + + Args: + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Additional data related to link. + + Raises: + HTTPRequestError: Server error happened. + """ + + if data is None: + data = {} + full_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type + ) + response = self.put( + "projects/{}/links/types/{}".format(project_name, full_type_name), + **data + ) + response.raise_for_status() + + def delete_link_type( + self, project_name, link_type_name, input_type, output_type + ): + """Remove link type from project. + + Args: + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + + Raises: + HTTPRequestError: Server error happened. + """ + + full_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type + ) + response = self.delete( + "projects/{}/links/types/{}".format(project_name, full_type_name)) + response.raise_for_status() + + def make_sure_link_type_exists( + self, project_name, link_type_name, input_type, output_type, data=None + ): + """Make sure link type exists on a project. + + Args: + project_name (str): Name of project. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Link type related data. + """ + + link_type = self.get_link_type( + project_name, link_type_name, input_type, output_type) + if ( + link_type + and (data is None or data == link_type["data"]) + ): + return + self.create_link_type( + project_name, link_type_name, input_type, output_type, data + ) + + def create_link( + self, + project_name, + link_type_name, + input_id, + input_type, + output_id, + output_type + ): + """Create link between 2 entities. + + Link has a type which must already exists on a project. + + Example output: + { + "id": "59a212c0d2e211eda0e20242ac120002" + } + + Args: + project_name (str): Project where the link is created. + link_type_name (str): Type of link. + input_id (str): Id of input entity. + input_type (str): Entity type of input entity. + output_id (str): Id of output entity. + output_type (str): Entity type of output entity. + + Returns: + dict[str, str]: Information about link. + + Raises: + HTTPRequestError: Server error happened. + """ + + full_link_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type) + response = self.post( + "projects/{}/links".format(project_name), + link=full_link_type_name, + input=input_id, + output=output_id + ) + response.raise_for_status() + return response.data + + def delete_link(self, project_name, link_id): + """Remove link by id. + + Args: + project_name (str): Project where link exists. + link_id (str): Id of link. + + Raises: + HTTPRequestError: Server error happened. + """ + + response = self.delete( + "projects/{}/links/{}".format(project_name, link_id) + ) + response.raise_for_status() + + def _prepare_link_filters(self, filters, link_types, link_direction): + """Add links filters for GraphQl queries. + + Args: + filters (dict[str, Any]): Object where filters will be added. + link_types (Union[Iterable[str], None]): Link types filters. + link_direction (Union[Literal["in", "out"], None]): Direction of + link "in", "out" or 'None' for both. + + Returns: + bool: Links are valid, and query from server can happen. + """ + + if link_types is not None: + link_types = set(link_types) + if not link_types: + return False + filters["linkTypes"] = list(link_types) + + if link_direction is not None: + if link_direction not in ("in", "out"): + return False + filters["linkDirection"] = link_direction + return True + + def get_entities_links( + self, + project_name, + entity_type, + entity_ids=None, + link_types=None, + link_direction=None + ): + """Helper method to get links from server for entity types. + + Example output: + [ + { + "id": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference", + "description": "reference link between folders", + "projectName": "my_project", + "author": "frantadmin", + "entityId": "b1df109676db11ed8e8c6c9466b19aa8", + "entityType": "folder", + "direction": "out" + }, + ... + ] + + Args: + project_name (str): Project where links are. + entity_type (Literal["folder", "task", "subset", + "version", "representations"]): Entity type. + entity_ids (Union[Iterable[str], None]): Ids of entities for which + links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by entity ids. + """ + + mapped_type = self._entity_types_link_mapping.get(entity_type) + if not mapped_type: + raise ValueError("Unknown type \"{}\". Expected {}".format( + entity_type, ", ".join(self._entity_types_link_mapping.keys()) + )) + + id_filter_key, project_sub_key = mapped_type + output = collections.defaultdict(list) + filters = { + "projectName": project_name + } + if entity_ids is not None: + entity_ids = set(entity_ids) + if not entity_ids: + return output + filters[id_filter_key] = list(entity_ids) + + if not self._prepare_link_filters(filters, link_types, link_direction): + return output + + query = folders_graphql_query({"id", "links"}) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for entity in parsed_data["project"][project_sub_key]: + entity_id = entity["id"] + output[entity_id].extend(entity["links"]) + return output + + def get_folders_links( + self, + project_name, + folder_ids=None, + link_types=None, + link_direction=None + ): + """Query folders links from server. + + Args: + project_name (str): Project where links are. + folder_ids (Union[Iterable[str], None]): Ids of folders for which + links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by folder ids. + """ + + return self.get_entities_links( + project_name, "folder", folder_ids, link_types, link_direction + ) + + def get_folder_links( + self, + project_name, + folder_id, + link_types=None, + link_direction=None + ): + """Query folder links from server. + + Args: + project_name (str): Project where links are. + folder_id (str): Id of folder for which links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of folder. + """ + + return self.get_folders_links( + project_name, [folder_id], link_types, link_direction + )[folder_id] + + def get_tasks_links( + self, + project_name, + task_ids=None, + link_types=None, + link_direction=None + ): + """Query tasks links from server. + + Args: + project_name (str): Project where links are. + task_ids (Union[Iterable[str], None]): Ids of tasks for which + links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by task ids. + """ + + return self.get_entities_links( + project_name, "task", task_ids, link_types, link_direction + ) + + def get_task_links( + self, + project_name, + task_id, + link_types=None, + link_direction=None + ): + """Query task links from server. + + Args: + project_name (str): Project where links are. + task_id (str): Id of task for which links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of task. + """ + + return self.get_tasks_links( + project_name, [task_id], link_types, link_direction + )[task_id] + + def get_subsets_links( + self, + project_name, + subset_ids=None, + link_types=None, + link_direction=None + ): + """Query subsets links from server. + + Args: + project_name (str): Project where links are. + subset_ids (Union[Iterable[str], None]): Ids of subsets for which + links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by subset ids. + """ + + return self.get_entities_links( + project_name, "subset", subset_ids, link_types, link_direction + ) + + def get_subset_links( + self, + project_name, + subset_id, + link_types=None, + link_direction=None + ): + """Query subset links from server. + + Args: + project_name (str): Project where links are. + subset_id (str): Id of subset for which links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of subset. + """ + + return self.get_subsets_links( + project_name, [subset_id], link_types, link_direction + )[subset_id] + + def get_versions_links( + self, + project_name, + version_ids=None, + link_types=None, + link_direction=None + ): + """Query versions links from server. + + Args: + project_name (str): Project where links are. + version_ids (Union[Iterable[str], None]): Ids of versions for which + links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by version ids. + """ + + return self.get_entities_links( + project_name, "version", version_ids, link_types, link_direction + ) + + def get_version_links( + self, + project_name, + version_id, + link_types=None, + link_direction=None + ): + """Query version links from server. + + Args: + project_name (str): Project where links are. + version_id (str): Id of version for which links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of version. + """ + + return self.get_versions_links( + project_name, [version_id], link_types, link_direction + )[version_id] + + def get_representations_links( + self, + project_name, + representation_ids=None, + link_types=None, + link_direction=None + ): + """Query representations links from server. + + Args: + project_name (str): Project where links are. + representation_ids (Union[Iterable[str], None]): Ids of + representations for which links should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by representation ids. + """ + + return self.get_entities_links( + project_name, + "representation", + representation_ids, + link_types, + link_direction + ) + + def get_representation_links( + self, + project_name, + representation_id, + link_types=None, + link_direction=None + ): + """Query representation links from server. + + Args: + project_name (str): Project where links are. + representation_id (str): Id of representation for which links + should be received. + link_types (Union[Iterable[str], None]): Link type filters. + link_direction (Union[Literal["in", "out"], None]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of representation. + """ + + return self.get_representations_links( + project_name, [representation_id], link_types, link_direction + )[representation_id] + + # --- Batch operations processing --- def send_batch_operations( self, project_name, diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index a65f885820..c8ddddd97e 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.1.16" \ No newline at end of file +__version__ = "0.1.17" From ed9b25236e7ac59bcd1aeba7fd9c9b57fd591157 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Apr 2023 09:56:40 +0200 Subject: [PATCH 214/446] AYON: Implement integrate links publish plugin (#4842) * implemented get_output_link_versions * implemented remainin entity link functions * implemented ayon variant of links integration * Changed class name and label * fix data types iteration * added links query fix from ayon api develop * removed unnecessary import --- openpype/client/server/entities.py | 19 ++- openpype/client/server/entity_links.py | 97 ++++++++++- .../plugins/publish/integrate_inputlinks.py | 6 + .../publish/integrate_inputlinks_ayon.py | 160 ++++++++++++++++++ .../python/common/ayon_api/server_api.py | 31 +++- .../vendor/python/common/ayon_api/version.py | 2 +- 6 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 openpype/plugins/publish/integrate_inputlinks_ayon.py diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index b49c8dd505..e9bb2287a0 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -423,8 +423,23 @@ def get_last_version_by_subset_name( ) -def get_output_link_versions(*args, **kwargs): - raise NotImplementedError("'get_output_link_versions' not implemented") +def get_output_link_versions(project_name, version_id, fields=None): + if not version_id: + return [] + + con = get_server_api_connection() + version_links = con.get_version_links( + project_name, version_id, link_direction="out") + + version_ids = { + link["entityId"] + for link in version_links + if link["entityType"] == "version" + } + if not version_ids: + return [] + + return get_versions(project_name, version_ids=version_ids, fields=fields) def version_is_latest(project_name, version_id): diff --git a/openpype/client/server/entity_links.py b/openpype/client/server/entity_links.py index f61b461f38..d8395aabe7 100644 --- a/openpype/client/server/entity_links.py +++ b/openpype/client/server/entity_links.py @@ -1,3 +1,9 @@ +import ayon_api +from ayon_api import get_folder_links, get_versions_links + +from .entities import get_assets, get_representation_by_id + + def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None): """Extract linked asset ids from asset document. @@ -15,7 +21,19 @@ def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None): List[Union[ObjectId, str]]: Asset ids of input links. """ - return [] + output = [] + if not asset_doc and not asset_id: + return output + + if not asset_id: + asset_id = asset_doc["_id"] + + links = get_folder_links(project_name, asset_id, link_direction="in") + return [ + link["entityId"] + for link in links + if link["entityType"] == "folder" + ] def get_linked_assets( @@ -38,7 +56,11 @@ def get_linked_assets( asset doc. """ - return [] + link_ids = get_linked_asset_ids(project_name, asset_doc, asset_id) + if not link_ids: + return [] + return list(get_assets(project_name, asset_ids=link_ids, fields=fields)) + def get_linked_representation_id( @@ -51,6 +73,10 @@ def get_linked_representation_id( Representation links now works only from representation through version back to representations. + Todos: + Missing depth query. Not sure how it did find more representations in + depth, probably links to version? + Args: project_name (str): Name of project where look for links. repre_doc (Dict[str, Any]): Representation document. @@ -62,4 +88,69 @@ def get_linked_representation_id( List[ObjectId] Linked representation ids. """ - return [] + if repre_doc: + repre_id = repre_doc["_id"] + + if not repre_id and not repre_doc: + return [] + + version_id = None + if repre_doc: + version_id = repre_doc.get("parent") + + if not version_id: + repre_doc = get_representation_by_id( + project_name, repre_id, fields=["parent"] + ) + if repre_doc: + version_id = repre_doc["parent"] + + if not version_id: + return [] + + if max_depth is None or max_depth == 0: + max_depth = 1 + + link_types = None + if link_type: + link_types = [link_type] + + # Store already found version ids to avoid recursion, and also to store + # output -> Don't forget to remove 'version_id' at the end!!! + linked_version_ids = {version_id} + # Each loop of depth will reset this variable + versions_to_check = {version_id} + for _ in range(max_depth): + if not versions_to_check: + break + + links = get_versions_links( + project_name, + versions_to_check, + link_types=link_types, + link_direction="out") + + versions_to_check = set() + for link in links: + # Care only about version links + if link["entityType"] != "version": + continue + entity_id = link["entityId"] + # Skip already found linked version ids + if entity_id in linked_version_ids: + continue + linked_version_ids.add(entity_id) + versions_to_check.add(entity_id) + + linked_version_ids.remove(version_id) + if not linked_version_ids: + return [] + + representations = ayon_api.get_representations( + project_name, + version_ids=linked_version_ids, + fields=["id"]) + return [ + repre["id"] + for repre in representations + ] diff --git a/openpype/plugins/publish/integrate_inputlinks.py b/openpype/plugins/publish/integrate_inputlinks.py index 6964f2d938..c639bbf994 100644 --- a/openpype/plugins/publish/integrate_inputlinks.py +++ b/openpype/plugins/publish/integrate_inputlinks.py @@ -3,6 +3,7 @@ from collections import OrderedDict from bson.objectid import ObjectId import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import legacy_io @@ -34,6 +35,11 @@ class IntegrateInputLinks(pyblish.api.ContextPlugin): plugin. """ + + if AYON_SERVER_ENABLED: + self.log.info("Skipping, in AYON mode") + return + workfile = None publishing = [] diff --git a/openpype/plugins/publish/integrate_inputlinks_ayon.py b/openpype/plugins/publish/integrate_inputlinks_ayon.py new file mode 100644 index 0000000000..8ab5c923c4 --- /dev/null +++ b/openpype/plugins/publish/integrate_inputlinks_ayon.py @@ -0,0 +1,160 @@ +import collections + +import pyblish.api +from ayon_api import create_link, make_sure_link_type_exists + +from openpype import AYON_SERVER_ENABLED + + +class IntegrateInputLinksAYON(pyblish.api.ContextPlugin): + """Connecting version level dependency links""" + + order = pyblish.api.IntegratorOrder + 0.2 + label = "Connect Dependency InputLinks AYON" + + def process(self, context): + """Connect dependency links for all instances, globally + + Code steps: + - filter instances that integrated version + - have "versionEntity" entry in data + - separate workfile instance within filtered instances + - when workfile instance is available: + - link all `loadedVersions` as input of the workfile + - link workfile as input of all other integrated versions + - link version's inputs if it's instance have "inputVersions" entry + - + + inputVersions: + The "inputVersions" in instance.data should be a list of + version ids (str), which are the dependencies of the publishing + instance that should be extracted from working scene by the DCC + specific publish plugin. + """ + + if not AYON_SERVER_ENABLED: + self.log.info("Skipping, not in AYON mode") + return + + workfile_instance, other_instances = self.split_instances(context) + + # Variable where links are stored in submethods + new_links_by_type = collections.defaultdict(list) + + self.create_workfile_links( + workfile_instance, other_instances, new_links_by_type) + + self.create_generative_links(other_instances, new_links_by_type) + + self.create_links_on_server(context, new_links_by_type) + + def split_instances(self, context): + workfile_instance = None + other_instances = [] + + for instance in context: + # Skip inactive instances + if not instance.data.get("publish", True): + continue + + version_doc = instance.data.get("versionEntity") + if not version_doc: + self.log.debug( + "Instance {} doesn't have version.".format(instance)) + continue + + family = instance.data.get("family") + if family == "workfile": + workfile_instance = instance + else: + other_instances.append(instance) + return workfile_instance, other_instances + + def add_link(self, new_links_by_type, link_type, input_id, output_id): + """Add dependency link data into temporary variable. + + Args: + new_links_by_type (dict[str, list[dict[str, Any]]]): Object where + output is stored. + link_type (str): Type of link, one of 'reference' or 'generative' + input_id (str): Input version id. + output_id (str): Output version id. + """ + + new_links_by_type[link_type].append((input_id, output_id)) + + def create_workfile_links( + self, workfile_instance, other_instances, new_links_by_type + ): + if workfile_instance is None: + self.log.warn("No workfile in this publish session.") + return + + workfile_version_id = workfile_instance.data["versionEntity"]["_id"] + # link workfile to all publishing versions + for instance in other_instances: + self.add_link( + new_links_by_type, + "generative", + workfile_version_id, + instance.data["versionEntity"]["_id"], + ) + + loaded_versions = workfile_instance.context.get("loadedVersions") + if not loaded_versions: + return + + # link all loaded versions in scene into workfile + for version in loaded_versions: + self.add_link( + new_links_by_type, + "reference", + version["version"], + workfile_version_id, + ) + + def create_generative_links(self, other_instances, new_links_by_type): + for instance in other_instances: + input_versions = instance.data.get("inputVersions") + if not input_versions: + continue + + version_entity = instance.data["versionEntity"] + for input_version in input_versions: + self.add_link( + new_links_by_type, + "generative", + input_version, + version_entity["_id"], + ) + + def create_links_on_server(self, context, new_links): + """Create new links on server. + + Args: + dict[str, list[tuple[str, str]]]: Version links by link type. + """ + + if not new_links: + return + + project_name = context.data["projectName"] + + # Make sure link types are available on server + for link_type in new_links.keys(): + make_sure_link_type_exists( + project_name, link_type, "version", "version" + ) + + # Create link themselves + for link_type, items in new_links.items(): + for item in items: + input_id, output_id = item + create_link( + project_name, + link_type, + input_id, + "version", + output_id, + "version" + ) diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index 8d52b484d8..796ec13d41 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -4514,13 +4514,34 @@ class ServerAPI(object): dict[str, list[dict[str, Any]]]: Link info by entity ids. """ - mapped_type = self._entity_types_link_mapping.get(entity_type) - if not mapped_type: + if entity_type == "folder": + query_func = folders_graphql_query + id_filter_key = "folderIds" + project_sub_key = "folders" + elif entity_type == "task": + query_func = tasks_graphql_query + id_filter_key = "taskIds" + project_sub_key = "tasks" + elif entity_type == "subset": + query_func = subsets_graphql_query + id_filter_key = "subsetIds" + project_sub_key = "subsets" + elif entity_type == "version": + query_func = versions_graphql_query + id_filter_key = "versionIds" + project_sub_key = "versions" + elif entity_type == "representation": + query_func = representations_graphql_query + id_filter_key = "representationIds" + project_sub_key = "representations" + else: raise ValueError("Unknown type \"{}\". Expected {}".format( - entity_type, ", ".join(self._entity_types_link_mapping.keys()) + entity_type, + ", ".join( + ("folder", "task", "subset", "version", "representation") + ) )) - id_filter_key, project_sub_key = mapped_type output = collections.defaultdict(list) filters = { "projectName": project_name @@ -4534,7 +4555,7 @@ class ServerAPI(object): if not self._prepare_link_filters(filters, link_types, link_direction): return output - query = folders_graphql_query({"id", "links"}) + query = query_func({"id", "links"}) for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index c8ddddd97e..3120942636 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.1.17" +__version__ = "0.1.17-1" From 1665ceff7808726bace29f34da6e5fd51027fb0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 27 Apr 2023 14:14:54 +0200 Subject: [PATCH 215/446] :recycle: change logic Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index 2160f03493..3b020c76c0 100644 --- a/start.py +++ b/start.py @@ -1051,7 +1051,7 @@ def boot(): if "validate" in commands: valid = _boot_validate_versions(use_version, local_version) - sys.exit(1 if not valid else 0) + sys.exit(0 if valid else 1) if not openpype_path: _print("*** Cannot get OpenPype path from database.") From 532ba40f68047402af046539fcd416f0225f7e60 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 13 Apr 2023 17:47:16 +0200 Subject: [PATCH 216/446] Updated gazu to 0.9.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 51895c20d1..0cb7fb010b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ Click = "^7" dnspython = "^2.1.0" ftrack-python-api = "^2.3.3" shotgun_api3 = {git = "https://github.com/shotgunsoftware/python-api.git", rev = "v3.3.3"} -gazu = "^0.8.34" +gazu = "^0.9.3" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) jsonschema = "^2.6.0" keyring = "^22.0.1" From b7c40966675af29303ca9cf133d43069b5b363a1 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 13 Apr 2023 17:47:56 +0200 Subject: [PATCH 217/446] "poetry lock"-update --- poetry.lock | 1244 +++++++++++++++++++++++++++------------------------ 1 file changed, 659 insertions(+), 585 deletions(-) diff --git a/poetry.lock b/poetry.lock index f915832fb8..d17e104d69 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,106 +18,106 @@ resolved_reference = "126f7a188cfe36718f707f42ebbc597e86aa86c3" [[package]] name = "aiohttp" -version = "3.8.3" +version = "3.8.4" description = "Async http client/server framework (asyncio)" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"}, - {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"}, - {file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"}, - {file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"}, - {file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"}, - {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"}, - {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"}, - {file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"}, - {file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"}, - {file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"}, - {file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"}, - {file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"}, - {file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"}, - {file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"}, - {file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"}, - {file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"}, - {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"}, - {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"}, - {file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"}, - {file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"}, - {file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"}, - {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"}, - {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"}, - {file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"}, - {file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"}, - {file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"}, - {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea"}, + {file = "aiohttp-3.8.4-cp310-cp310-win32.whl", hash = "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1"}, + {file = "aiohttp-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24"}, + {file = "aiohttp-3.8.4-cp311-cp311-win32.whl", hash = "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d"}, + {file = "aiohttp-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc"}, + {file = "aiohttp-3.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff"}, + {file = "aiohttp-3.8.4-cp36-cp36m-win32.whl", hash = "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777"}, + {file = "aiohttp-3.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e"}, + {file = "aiohttp-3.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241"}, + {file = "aiohttp-3.8.4-cp37-cp37m-win32.whl", hash = "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a"}, + {file = "aiohttp-3.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d"}, + {file = "aiohttp-3.8.4-cp38-cp38-win32.whl", hash = "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54"}, + {file = "aiohttp-3.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4"}, + {file = "aiohttp-3.8.4-cp39-cp39-win32.whl", hash = "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a"}, + {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"}, + {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"}, ] [package.dependencies] aiosignal = ">=1.1.2" async-timeout = ">=4.0.0a3,<5.0" attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<3.0" +charset-normalizer = ">=2.0,<4.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" @@ -176,7 +176,7 @@ frozenlist = ">=1.1.0" name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -210,7 +210,7 @@ develop = false type = "git" url = "https://github.com/ActiveState/appdirs.git" reference = "master" -resolved_reference = "211708144ddcbba1f02e26a43efec9aef57bc9fc" +resolved_reference = "8734277956c1df3b85385e6b308e954910533884" [[package]] name = "arrow" @@ -229,19 +229,19 @@ python-dateutil = ">=2.7.0" [[package]] name = "astroid" -version = "2.13.2" +version = "2.15.2" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "astroid-2.13.2-py3-none-any.whl", hash = "sha256:8f6a8d40c4ad161d6fc419545ae4b2f275ed86d1c989c97825772120842ee0d2"}, - {file = "astroid-2.13.2.tar.gz", hash = "sha256:3bc7834720e1a24ca797fd785d77efb14f7a28ee8e635ef040b6e2d80ccb3303"}, + {file = "astroid-2.15.2-py3-none-any.whl", hash = "sha256:dea89d9f99f491c66ac9c04ebddf91e4acf8bd711722175fe6245c0725cc19bb"}, + {file = "astroid-2.15.2.tar.gz", hash = "sha256:6e61b85c891ec53b07471aec5878f4ac6446a41e590ede0f2ce095f39f7d49dd"}, ] [package.dependencies] lazy-object-proxy = ">=1.4.0" -typing-extensions = ">=4.0.0" +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} [[package]] @@ -288,14 +288,14 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy [[package]] name = "autopep8" -version = "2.0.1" +version = "2.0.2" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "autopep8-2.0.1-py2.py3-none-any.whl", hash = "sha256:be5bc98c33515b67475420b7b1feafc8d32c1a69862498eda4983b45bffd2687"}, - {file = "autopep8-2.0.1.tar.gz", hash = "sha256:d27a8929d8dcd21c0f4b3859d2d07c6c25273727b98afc984c039df0f0d86566"}, + {file = "autopep8-2.0.2-py2.py3-none-any.whl", hash = "sha256:86e9303b5e5c8160872b2f5ef611161b2893e9bfe8ccc7e2f76385947d57a2f1"}, + {file = "autopep8-2.0.2.tar.gz", hash = "sha256:f9849cdd62108cb739dbcdbfb7fdcc9a30d1b63c4cc3e1c1f893b5360941b61c"}, ] [package.dependencies] @@ -304,14 +304,14 @@ tomli = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "ayon-python-api" -version = "0.1.16" +version = "0.1.17" description = "AYON Python API" category = "main" optional = false python-versions = "*" files = [ - {file = "ayon-python-api-0.1.16.tar.gz", hash = "sha256:666110954dd75b2be1699a29b4732cfb0bcb09d01f64fba4449bfc8ac1fb43f1"}, - {file = "ayon_python_api-0.1.16-py3-none-any.whl", hash = "sha256:bbcd6df1f80ddf32e653a1bb31289cb5fd1a8bea36ab4c8e6aef08c41b6393de"}, + {file = "ayon-python-api-0.1.17.tar.gz", hash = "sha256:f45060f4f8dd26c825823dab00dfc0c3add8309343d823663233f34ca118550a"}, + {file = "ayon_python_api-0.1.17-py3-none-any.whl", hash = "sha256:5d3b11ecca3f30856bb403000009bad2002e72c200cad97b4a9c9d05259c14af"}, ] [package.dependencies] @@ -322,19 +322,16 @@ Unidecode = ">=1.2.0" [[package]] name = "babel" -version = "2.11.0" +version = "2.12.1" description = "Internationalization utilities" -category = "main" +category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, - {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, ] -[package.dependencies] -pytz = ">=2015.7" - [[package]] name = "bcrypt" version = "4.0.1" @@ -370,16 +367,33 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "bidict" +version = "0.22.1" +description = "The bidirectional mapping library for Python." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bidict-0.22.1-py3-none-any.whl", hash = "sha256:6ef212238eb884b664f28da76f33f1d28b260f665fc737b413b287d5487d1e7b"}, + {file = "bidict-0.22.1.tar.gz", hash = "sha256:1e0f7f74e4860e6d0943a05d4134c63a2fad86f3d4732fb265bd79e4e856d81d"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton"] +lint = ["pre-commit"] +test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "pytest-xdist", "sortedcollections", "sortedcontainers", "sphinx"] + [[package]] name = "blessed" -version = "1.19.1" +version = "1.20.0" description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." category = "main" optional = false python-versions = ">=2.7" files = [ - {file = "blessed-1.19.1-py2.py3-none-any.whl", hash = "sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b"}, - {file = "blessed-1.19.1.tar.gz", hash = "sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc"}, + {file = "blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058"}, + {file = "blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"}, ] [package.dependencies] @@ -389,14 +403,14 @@ wcwidth = ">=0.1.4" [[package]] name = "cachetools" -version = "5.2.1" +version = "5.3.0" description = "Extensible memoizing collections and decorators" category = "main" optional = false python-versions = "~=3.7" files = [ - {file = "cachetools-5.2.1-py3-none-any.whl", hash = "sha256:8462eebf3a6c15d25430a8c27c56ac61340b2ecf60c9ce57afc2b97e450e47da"}, - {file = "cachetools-5.2.1.tar.gz", hash = "sha256:5991bc0e08a1319bb618d3195ca5b6bc76646a49c21d55962977197b301cc1fe"}, + {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"}, + {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"}, ] [[package]] @@ -502,19 +516,89 @@ files = [ [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, ] -[package.extras] -unicode-backport = ["unicodedata2"] - [[package]] name = "click" version = "7.1.2" @@ -548,7 +632,7 @@ test = ["pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -585,63 +669,63 @@ files = [ [[package]] name = "coverage" -version = "7.0.5" +version = "7.2.3" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b"}, - {file = "coverage-7.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809"}, - {file = "coverage-7.0.5-cp310-cp310-win32.whl", hash = "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21"}, - {file = "coverage-7.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad"}, - {file = "coverage-7.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca"}, - {file = "coverage-7.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095"}, - {file = "coverage-7.0.5-cp311-cp311-win32.whl", hash = "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831"}, - {file = "coverage-7.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea"}, - {file = "coverage-7.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47"}, - {file = "coverage-7.0.5-cp37-cp37m-win32.whl", hash = "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882"}, - {file = "coverage-7.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d"}, - {file = "coverage-7.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb"}, - {file = "coverage-7.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499"}, - {file = "coverage-7.0.5-cp38-cp38-win32.whl", hash = "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16"}, - {file = "coverage-7.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af"}, - {file = "coverage-7.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab"}, - {file = "coverage-7.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1"}, - {file = "coverage-7.0.5-cp39-cp39-win32.whl", hash = "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904"}, - {file = "coverage-7.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f"}, - {file = "coverage-7.0.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0"}, - {file = "coverage-7.0.5.tar.gz", hash = "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45"}, + {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"}, + {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"}, + {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"}, + {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"}, + {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"}, + {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"}, + {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"}, + {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"}, + {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"}, + {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"}, + {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"}, + {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"}, + {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"}, + {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"}, + {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"}, + {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"}, + {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"}, + {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"}, + {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"}, + {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"}, + {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"}, ] [package.dependencies] @@ -652,47 +736,45 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "39.0.0" +version = "40.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"}, - {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"}, - {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"}, - {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"}, - {file = "cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"}, - {file = "cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"}, - {file = "cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"}, + {file = "cryptography-40.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:918cb89086c7d98b1b86b9fdb70c712e5a9325ba6f7d7cfb509e784e0cfc6917"}, + {file = "cryptography-40.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9618a87212cb5200500e304e43691111570e1f10ec3f35569fdfcd17e28fd797"}, + {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4805a4ca729d65570a1b7cac84eac1e431085d40387b7d3bbaa47e39890b88"}, + {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63dac2d25c47f12a7b8aa60e528bfb3c51c5a6c5a9f7c86987909c6c79765554"}, + {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0a4e3406cfed6b1f6d6e87ed243363652b2586b2d917b0609ca4f97072994405"}, + {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1e0af458515d5e4028aad75f3bb3fe7a31e46ad920648cd59b64d3da842e4356"}, + {file = "cryptography-40.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d8aa3609d337ad85e4eb9bb0f8bcf6e4409bfb86e706efa9a027912169e89122"}, + {file = "cryptography-40.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cf91e428c51ef692b82ce786583e214f58392399cf65c341bc7301d096fa3ba2"}, + {file = "cryptography-40.0.1-cp36-abi3-win32.whl", hash = "sha256:650883cc064297ef3676b1db1b7b1df6081794c4ada96fa457253c4cc40f97db"}, + {file = "cryptography-40.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:a805a7bce4a77d51696410005b3e85ae2839bad9aa38894afc0aa99d8e0c3160"}, + {file = "cryptography-40.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd033d74067d8928ef00a6b1327c8ea0452523967ca4463666eeba65ca350d4c"}, + {file = "cryptography-40.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d36bbeb99704aabefdca5aee4eba04455d7a27ceabd16f3b3ba9bdcc31da86c4"}, + {file = "cryptography-40.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:32057d3d0ab7d4453778367ca43e99ddb711770477c4f072a51b3ca69602780a"}, + {file = "cryptography-40.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f5d7b79fa56bc29580faafc2ff736ce05ba31feaa9d4735048b0de7d9ceb2b94"}, + {file = "cryptography-40.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7c872413353c70e0263a9368c4993710070e70ab3e5318d85510cc91cce77e7c"}, + {file = "cryptography-40.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:28d63d75bf7ae4045b10de5413fb1d6338616e79015999ad9cf6fc538f772d41"}, + {file = "cryptography-40.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6f2bbd72f717ce33100e6467572abaedc61f1acb87b8d546001328d7f466b778"}, + {file = "cryptography-40.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cc3a621076d824d75ab1e1e530e66e7e8564e357dd723f2533225d40fe35c60c"}, + {file = "cryptography-40.0.1.tar.gz", hash = "sha256:2803f2f8b1e95f614419926c7e6f55d828afc614ca5ed61543877ae668cc3472"}, ] [package.dependencies] cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "ruff"] +pep8test = ["black", "check-manifest", "mypy", "ruff"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] +test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] +tox = ["tox"] [[package]] name = "cx-freeze" @@ -783,24 +865,6 @@ files = [ {file = "cx_Logging-3.1.0.tar.gz", hash = "sha256:8a06834d8527aa904a68b25c9c1a5fa09f0dfdc94dbd9f86b81cd8d2f7a0e487"}, ] -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] - [[package]] name = "dill" version = "0.3.6" @@ -853,7 +917,7 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"] name = "docutils" version = "0.19" description = "Docutils -- Python Documentation Utilities" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -881,14 +945,14 @@ stone = ">=2" [[package]] name = "enlighten" -version = "1.11.1" +version = "1.11.2" description = "Enlighten Progress Bar" category = "main" optional = false python-versions = "*" files = [ - {file = "enlighten-1.11.1-py2.py3-none-any.whl", hash = "sha256:e825eb534ca80778bb7d46e5581527b2a6fae559b6cf09e290a7952c6e11961e"}, - {file = "enlighten-1.11.1.tar.gz", hash = "sha256:57abd98a3d3f83484ef9f91f9255f4d23c8b3097ecdb647c7b9b0049d600b7f8"}, + {file = "enlighten-1.11.2-py2.py3-none-any.whl", hash = "sha256:98c9eb20e022b6a57f1c8d4f17e16760780b6881e6d658c40f52d21255ea45f3"}, + {file = "enlighten-1.11.2.tar.gz", hash = "sha256:9284861dee5a272e0e1a3758cd3f3b7180b1bd1754875da76876f2a7f46ccb61"}, ] [package.dependencies] @@ -897,30 +961,30 @@ prefixed = ">=0.3.2" [[package]] name = "evdev" -version = "1.6.0" +version = "1.6.1" description = "Bindings to the Linux input handling subsystem" category = "main" optional = false python-versions = "*" files = [ - {file = "evdev-1.6.0.tar.gz", hash = "sha256:ecfa01b5c84f7e8c6ced3367ac95288f43cd84efbfd7dd7d0cdbfc0d18c87a6a"}, + {file = "evdev-1.6.1.tar.gz", hash = "sha256:299db8628cc73b237fc1cc57d3c2948faa0756e2a58b6194b5bf81dc2081f1e3"}, ] [[package]] name = "filelock" -version = "3.9.0" +version = "3.11.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, - {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, + {file = "filelock-3.11.0-py3-none-any.whl", hash = "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"}, + {file = "filelock-3.11.0.tar.gz", hash = "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37"}, ] [package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -1025,14 +1089,14 @@ files = [ [[package]] name = "ftrack-python-api" -version = "2.3.3" +version = "2.4.2" description = "Python API for ftrack." category = "main" optional = false -python-versions = ">=2.7.9, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, < 3.10" +python-versions = ">=2.7.9, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "ftrack-python-api-2.3.3.tar.gz", hash = "sha256:358f37e5b1c5635eab107c19e27a0c890d512877f78af35b1ac416e90c037295"}, - {file = "ftrack_python_api-2.3.3-py2.py3-none-any.whl", hash = "sha256:82834c4d5def5557a2ea547a7e6f6ba84d3129e8f90457d8bbd85b287a2c39f6"}, + {file = "ftrack-python-api-2.4.2.tar.gz", hash = "sha256:4bc85ac90421dd70cd0c513534677d40898ec1554104e88267046f6b634bb145"}, + {file = "ftrack_python_api-2.4.2-py2.py3-none-any.whl", hash = "sha256:a8d8cdd9337d83fe4636081dfd0b74cc2552b50531c7fb58efa194b5aeb807bd"}, ] [package.dependencies] @@ -1059,23 +1123,23 @@ files = [ [[package]] name = "gazu" -version = "0.8.34" +version = "0.9.3" description = "Gazu is a client for Zou, the API to store the data of your CG production." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*, != 3.4.*, != 3.5.*, != 3.6.1, != 3.6.2" files = [ - {file = "gazu-0.8.34-py2.py3-none-any.whl", hash = "sha256:a78a8c5e61108aeaab6185646af78b0402dbdb29097e8ba5882bd55410f38c4b"}, + {file = "gazu-0.9.3-py2.py3-none-any.whl", hash = "sha256:daa6e4bdaa364b68a048ad97837aec011a0060d12edc3a5ac6ae34c13a05cb2b"}, ] [package.dependencies] -deprecated = "1.2.13" -python-socketio = {version = "4.6.1", extras = ["client"], markers = "python_version >= \"3.5\""} -requests = ">=2.25.1,<=2.28.1" +python-socketio = {version = "5.8.0", extras = ["client"], markers = "python_version != \"2.7\""} +requests = ">=2.25.1" [package.extras] dev = ["wheel"] -test = ["black (<=22.8.0)", "pre-commit (<=2.20.0)", "pytest (<=7.1.3)", "pytest-cov (<=3.0.0)", "requests-mock (==1.10.0)"] +lint = ["black (==23.3.0)", "pre-commit (==3.2.2)"] +test = ["pytest", "pytest-cov", "requests-mock"] [[package]] name = "gitdb" @@ -1094,14 +1158,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.30" -description = "GitPython is a python library used to interact with Git repositories" +version = "3.1.31" +description = "GitPython is a Python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"}, - {file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"}, + {file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"}, + {file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"}, ] [package.dependencies] @@ -1152,14 +1216,14 @@ uritemplate = ">=3.0.0,<4dev" [[package]] name = "google-auth" -version = "2.16.0" +version = "2.17.3" description = "Google Authentication Library" category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ - {file = "google-auth-2.16.0.tar.gz", hash = "sha256:ed7057a101af1146f0554a769930ac9de506aeca4fd5af6543ebe791851a9fbd"}, - {file = "google_auth-2.16.0-py2.py3-none-any.whl", hash = "sha256:5045648c821fb72384cdc0e82cc326df195f113a33049d9b62b74589243d2acc"}, + {file = "google-auth-2.17.3.tar.gz", hash = "sha256:ce311e2bc58b130fddf316df57c9b3943c2a7b4f6ec31de9663a9333e4064efc"}, + {file = "google_auth-2.17.3-py2.py3-none-any.whl", hash = "sha256:f586b274d3eb7bd932ea424b1c702a30e0393a2e2bc4ca3eae8263ffd8be229f"}, ] [package.dependencies] @@ -1194,14 +1258,14 @@ six = "*" [[package]] name = "googleapis-common-protos" -version = "1.58.0" +version = "1.59.0" description = "Common protobufs used in Google APIs" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.58.0.tar.gz", hash = "sha256:c727251ec025947d545184ba17e3578840fc3a24a0516a020479edab660457df"}, - {file = "googleapis_common_protos-1.58.0-py2.py3-none-any.whl", hash = "sha256:ca3befcd4580dab6ad49356b46bf165bb68ff4b32389f028f1abd7c10ab9519a"}, + {file = "googleapis-common-protos-1.59.0.tar.gz", hash = "sha256:4168fcb568a826a52f23510412da405abd93f4d23ba544bb68d943b14ba3cb44"}, + {file = "googleapis_common_protos-1.59.0-py2.py3-none-any.whl", hash = "sha256:b287dc48449d1d41af0c69f4ea26242b5ae4c3d7249a38b0984c86a4caffff1f"}, ] [package.dependencies] @@ -1212,14 +1276,14 @@ grpc = ["grpcio (>=1.44.0,<2.0.0dev)"] [[package]] name = "httplib2" -version = "0.21.0" +version = "0.22.0" description = "A comprehensive HTTP client library." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "httplib2-0.21.0-py3-none-any.whl", hash = "sha256:987c8bb3eb82d3fa60c68699510a692aa2ad9c4bd4f123e51dfb1488c14cdd01"}, - {file = "httplib2-0.21.0.tar.gz", hash = "sha256:fc144f091c7286b82bec71bdbd9b27323ba709cc612568d3000893bfd9cb4b34"}, + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, ] [package.dependencies] @@ -1227,14 +1291,14 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 [[package]] name = "identify" -version = "2.5.13" +version = "2.5.22" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "identify-2.5.13-py2.py3-none-any.whl", hash = "sha256:8aa48ce56e38c28b6faa9f261075dea0a942dfbb42b341b4e711896cbb40f3f7"}, - {file = "identify-2.5.13.tar.gz", hash = "sha256:abb546bca6f470228785338a01b539de8a85bbf46491250ae03363956d8ebb10"}, + {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"}, + {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"}, ] [package.extras] @@ -1256,7 +1320,7 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1266,14 +1330,14 @@ files = [ [[package]] name = "importlib-metadata" -version = "6.0.0" +version = "6.3.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, - {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, + {file = "importlib_metadata-6.3.0-py3-none-any.whl", hash = "sha256:8f8bd2af397cf33bd344d35cfe7f489219b7d14fc79a3f854b75b8417e9226b0"}, + {file = "importlib_metadata-6.3.0.tar.gz", hash = "sha256:23c2bcae4762dfb0bbe072d358faec24957901d75b6c4ab11172c0c982532402"}, ] [package.dependencies] @@ -1298,19 +1362,19 @@ files = [ [[package]] name = "isort" -version = "5.11.4" +version = "5.12.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, - {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, ] [package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile-deprecated-finder = ["pipreqs", "requirementslib"] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] @@ -1352,7 +1416,7 @@ trio = ["async_generator", "trio"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1474,11 +1538,13 @@ python-versions = ">=3.6" files = [ {file = "lief-0.12.3-cp310-cp310-macosx_10_14_arm64.whl", hash = "sha256:66724f337e6a36cea1a9380f13b59923f276c49ca837becae2e7be93a2e245d9"}, {file = "lief-0.12.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6d18aafa2028587c98f6d4387bec94346e92f2b5a8a5002f70b1cf35b1c045cc"}, + {file = "lief-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f69d125caaa8d5ddb574f29cc83101e165ebea1a9f18ad042eb3544081a797"}, {file = "lief-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c078d6230279ffd3bca717c79664fb8368666f610b577deb24b374607936e9c1"}, {file = "lief-0.12.3-cp310-cp310-win32.whl", hash = "sha256:e3a6af926532d0aac9e7501946134513d63217bacba666e6f7f5a0b7e15ba236"}, {file = "lief-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:0750b72e3aa161e1fb0e2e7f571121ae05d2428aafd742ff05a7656ad2288447"}, {file = "lief-0.12.3-cp311-cp311-macosx_10_14_arm64.whl", hash = "sha256:b5c123cb99a7879d754c059e299198b34e7e30e3b64cf22e8962013db0099f47"}, {file = "lief-0.12.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8bc58fa26a830df6178e36f112cb2bbdd65deff593f066d2d51434ff78386ba5"}, + {file = "lief-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74ac6143ac6ccd813c9b068d9c5f1f9d55c8813c8b407387eb57de01c3db2d74"}, {file = "lief-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04eb6b70d646fb5bd6183575928ee23715550f161f2832cbcd8c6ff2071fb408"}, {file = "lief-0.12.3-cp311-cp311-win32.whl", hash = "sha256:7e2d0a53c403769b04adcf8df92e83c5e25f9103a052aa7f17b0a9cf057735fb"}, {file = "lief-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:7f6395c12ee1bc4a5162f567cba96d0c72dfb660e7902e84d4f3029daf14fe33"}, @@ -1498,6 +1564,7 @@ files = [ {file = "lief-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:b00667257b43e93d94166c959055b6147d46d302598f3ee55c194b40414c89cc"}, {file = "lief-0.12.3-cp39-cp39-macosx_10_14_arm64.whl", hash = "sha256:e6a1b5b389090d524621c2455795e1262f62dc9381bedd96f0cd72b878c4066d"}, {file = "lief-0.12.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ae773196df814202c0c51056163a1478941b299512b09660a3c37be3c7fac81e"}, + {file = "lief-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:66ddf88917ec7b00752687c476bb2771dc8ec19bd7e4c0dcff1f8ef774cad4e9"}, {file = "lief-0.12.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4a47f410032c63ac3be051d963d0337d6b47f0e94bfe8e946ab4b6c428f4d0f8"}, {file = "lief-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbd11367c2259bd1131a6c8755dcde33314324de5ea029227bfbc7d3755871e6"}, {file = "lief-0.12.3-cp39-cp39-win32.whl", hash = "sha256:2ce53e311918c3e5b54c815ef420a747208d2a88200c41cd476f3dd1eb876bcf"}, @@ -1509,7 +1576,7 @@ files = [ name = "linkify-it-py" version = "2.0.0" description = "Links recognition library with FULL unicode support." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1545,7 +1612,7 @@ name = "m2r2" version = "0.3.3.post2" description = "Markdown and reStructuredText in a single file." category = "dev" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "m2r2-0.3.3.post2-py3-none-any.whl", hash = "sha256:86157721eb6eabcd54d4eea7195890cc58fa6188b8d0abea633383cfbb5e11e3"}, @@ -1558,24 +1625,24 @@ mistune = "0.8.4" [[package]] name = "markdown-it-py" -version = "2.1.0" +version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, - {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] -code-style = ["pre-commit (==2.6)"] -compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] @@ -1585,7 +1652,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1674,14 +1741,14 @@ files = [ [[package]] name = "mdit-py-plugins" -version = "0.3.3" +version = "0.3.5" description = "Collection of plugins for markdown-it-py" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mdit-py-plugins-0.3.3.tar.gz", hash = "sha256:5cfd7e7ac582a594e23ba6546a2f406e94e42eb33ae596d0734781261c251260"}, - {file = "mdit_py_plugins-0.3.3-py3-none-any.whl", hash = "sha256:36d08a29def19ec43acdcd8ba471d3ebab132e7879d442760d963f19913e04b9"}, + {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, + {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, ] [package.dependencies] @@ -1696,7 +1763,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1709,7 +1776,7 @@ name = "mistune" version = "0.8.4" description = "The fastest markdown parser in pure Python" category = "dev" -optional = true +optional = false python-versions = "*" files = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, @@ -1804,7 +1871,7 @@ files = [ name = "myst-parser" version = "0.18.1" description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1919,39 +1986,37 @@ view = ["PySide2 (>=5.11,<6.0)"] [[package]] name = "packaging" -version = "23.0" +version = "23.1" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] [[package]] name = "paramiko" -version = "2.12.0" +version = "3.1.0" description = "SSH2 protocol library" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "paramiko-2.12.0-py2.py3-none-any.whl", hash = "sha256:b2df1a6325f6996ef55a8789d0462f5b502ea83b3c990cbb5bbe57345c6812c4"}, - {file = "paramiko-2.12.0.tar.gz", hash = "sha256:376885c05c5d6aa6e1f4608aac2a6b5b0548b1add40274477324605903d9cd49"}, + {file = "paramiko-3.1.0-py3-none-any.whl", hash = "sha256:f0caa660e797d9cd10db6fc6ae81e2c9b2767af75c3180fcd0e46158cd368d7f"}, + {file = "paramiko-3.1.0.tar.gz", hash = "sha256:6950faca6819acd3219d4ae694a23c7a87ee38d084f70c1724b0c0dbb8b75769"}, ] [package.dependencies] -bcrypt = ">=3.1.3" -cryptography = ">=2.5" -pynacl = ">=1.0.1" -six = "*" +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" [package.extras] -all = ["bcrypt (>=3.1.3)", "gssapi (>=1.4.1)", "invoke (>=1.3)", "pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "pywin32 (>=2.1.8)"] -ed25519 = ["bcrypt (>=3.1.3)", "pynacl (>=1.0.1)"] +all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -invoke = ["invoke (>=1.3)"] +invoke = ["invoke (>=2.0)"] [[package]] name = "parso" @@ -1971,18 +2036,19 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "patchelf" -version = "0.17.2.0" +version = "0.17.2.1" description = "A small utility to modify the dynamic linker and RPATH of ELF executables." category = "dev" optional = false python-versions = "*" files = [ - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b8d86f32e1414d6964d5d166ddd2cf829d156fba0d28d32a3bd0192f987f4529"}, - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:9233a0f2fc73820c5bd468f27507bdf0c9ac543f07c7f9888bb7cf910b1be22f"}, - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:6601d7d831508bcdd3d8ebfa6435c2379bf11e41af2409ae7b88de572926841c"}, - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.musllinux_1_1_i686.whl", hash = "sha256:c62a34f0c25e6c2d6ae44389f819a00ccdf3f292ad1b814fbe1cc23cb27023ce"}, - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:1b9fd14f300341dc020ae05c49274dd1fa6727eabb4e61dd7fb6fb3600acd26e"}, - {file = "patchelf-0.17.2.0.tar.gz", hash = "sha256:dedf987a83d7f6d6f5512269e57f5feeec36719bd59567173b6d9beabe019efe"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:fc329da0e8f628bd836dfb8eaf523547e342351fa8f739bf2b3fe4a6db5a297c"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ccb266a94edf016efe80151172c26cff8c2ec120a57a1665d257b0442784195d"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:f47b5bdd6885cfb20abdd14c707d26eb6f499a7f52e911865548d4aa43385502"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:a9e6ebb0874a11f7ed56d2380bfaa95f00612b23b15f896583da30c2059fcfa8"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.musllinux_1_1_i686.whl", hash = "sha256:3c8d58f0e4c1929b1c7c45ba8da5a84a8f1aa6a82a46e1cfb2e44a4d40f350e5"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d1a9bc0d4fd80c038523ebdc451a1cce75237cfcc52dbd1aca224578001d5927"}, + {file = "patchelf-0.17.2.1.tar.gz", hash = "sha256:a6eb0dd452ce4127d0d5e1eb26515e39186fa609364274bc1b0b77539cfa7031"}, ] [package.extras] @@ -2005,103 +2071,99 @@ six = "*" [[package]] name = "pillow" -version = "9.4.0" +version = "9.5.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, - {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, - {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, - {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, - {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, - {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, - {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, - {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, - {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, - {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, - {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, - {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, - {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, - {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, - {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, - {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, - {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, - {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, - {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "platformdirs" -version = "2.6.2" +version = "3.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, - {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, + {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, + {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, ] [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -2136,7 +2198,7 @@ name = "pockets" version = "0.9.1" description = "A collection of helpful Python tools!" category = "dev" -optional = true +optional = false python-versions = "*" files = [ {file = "pockets-0.9.1-py2.py3-none-any.whl", hash = "sha256:68597934193c08a08eb2bf6a1d85593f627c22f9b065cc727a4f03f669d96d86"}, @@ -2148,14 +2210,14 @@ six = ">=1.5.2" [[package]] name = "pre-commit" -version = "2.21.0" +version = "3.2.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, - {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, + {file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"}, + {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"}, ] [package.dependencies] @@ -2167,38 +2229,37 @@ virtualenv = ">=20.10.0" [[package]] name = "prefixed" -version = "0.6.0" +version = "0.7.0" description = "Prefixed alternative numeric library" category = "main" optional = false python-versions = "*" files = [ - {file = "prefixed-0.6.0-py2.py3-none-any.whl", hash = "sha256:5ab094773dc71df68cc78151c81510b9521dcc6b58a4acb78442b127d4e400fa"}, - {file = "prefixed-0.6.0.tar.gz", hash = "sha256:b39fbfac72618fa1eeb5b3fd9ed1341f10dd90df75499cb4c38a6c3ef47cdd94"}, + {file = "prefixed-0.7.0-py2.py3-none-any.whl", hash = "sha256:537b0e4ff4516c4578f277a41d7104f769d6935ae9cdb0f88fed82ec7b3c0ca5"}, + {file = "prefixed-0.7.0.tar.gz", hash = "sha256:0b54d15e602eb8af4ac31b1db21a37ea95ce5890e0741bb0dd9ded493cefbbe9"}, ] [[package]] name = "protobuf" -version = "4.21.12" +version = "4.22.3" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.21.12-cp310-abi3-win32.whl", hash = "sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1"}, - {file = "protobuf-4.21.12-cp310-abi3-win_amd64.whl", hash = "sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2"}, - {file = "protobuf-4.21.12-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791"}, - {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97"}, - {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7"}, - {file = "protobuf-4.21.12-cp37-cp37m-win32.whl", hash = "sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717"}, - {file = "protobuf-4.21.12-cp37-cp37m-win_amd64.whl", hash = "sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"}, - {file = "protobuf-4.21.12-cp38-cp38-win32.whl", hash = "sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec"}, - {file = "protobuf-4.21.12-cp38-cp38-win_amd64.whl", hash = "sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30"}, - {file = "protobuf-4.21.12-cp39-cp39-win32.whl", hash = "sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc"}, - {file = "protobuf-4.21.12-cp39-cp39-win_amd64.whl", hash = "sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b"}, - {file = "protobuf-4.21.12-py2.py3-none-any.whl", hash = "sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5"}, - {file = "protobuf-4.21.12-py3-none-any.whl", hash = "sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462"}, - {file = "protobuf-4.21.12.tar.gz", hash = "sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab"}, + {file = "protobuf-4.22.3-cp310-abi3-win32.whl", hash = "sha256:8b54f56d13ae4a3ec140076c9d937221f887c8f64954673d46f63751209e839a"}, + {file = "protobuf-4.22.3-cp310-abi3-win_amd64.whl", hash = "sha256:7760730063329d42a9d4c4573b804289b738d4931e363ffbe684716b796bde51"}, + {file = "protobuf-4.22.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:d14fc1a41d1a1909998e8aff7e80d2a7ae14772c4a70e4bf7db8a36690b54425"}, + {file = "protobuf-4.22.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:70659847ee57a5262a65954538088a1d72dfc3e9882695cab9f0c54ffe71663b"}, + {file = "protobuf-4.22.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:13233ee2b9d3bd9a5f216c1fa2c321cd564b93d8f2e4f521a85b585447747997"}, + {file = "protobuf-4.22.3-cp37-cp37m-win32.whl", hash = "sha256:ecae944c6c2ce50dda6bf76ef5496196aeb1b85acb95df5843cd812615ec4b61"}, + {file = "protobuf-4.22.3-cp37-cp37m-win_amd64.whl", hash = "sha256:d4b66266965598ff4c291416be429cef7989d8fae88b55b62095a2331511b3fa"}, + {file = "protobuf-4.22.3-cp38-cp38-win32.whl", hash = "sha256:f08aa300b67f1c012100d8eb62d47129e53d1150f4469fd78a29fa3cb68c66f2"}, + {file = "protobuf-4.22.3-cp38-cp38-win_amd64.whl", hash = "sha256:f2f4710543abec186aee332d6852ef5ae7ce2e9e807a3da570f36de5a732d88e"}, + {file = "protobuf-4.22.3-cp39-cp39-win32.whl", hash = "sha256:7cf56e31907c532e460bb62010a513408e6cdf5b03fb2611e4b67ed398ad046d"}, + {file = "protobuf-4.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:e0e630d8e6a79f48c557cd1835865b593d0547dce221c66ed1b827de59c66c97"}, + {file = "protobuf-4.22.3-py3-none-any.whl", hash = "sha256:52f0a78141078077cfe15fe333ac3e3a077420b9a3f5d1bf9b5fe9d286b4d881"}, + {file = "protobuf-4.22.3.tar.gz", hash = "sha256:23452f2fdea754a8251d0fc88c0317735ae47217e0d27bf330a30eec2848811a"}, ] [[package]] @@ -2253,14 +2314,14 @@ pyasn1 = ">=0.4.6,<0.5.0" [[package]] name = "pyblish-base" -version = "1.8.8" +version = "1.8.11" description = "Plug-in driven automation framework for content" category = "main" optional = false python-versions = "*" files = [ - {file = "pyblish-base-1.8.8.tar.gz", hash = "sha256:85a2c034dbb86345bf95018f5b7b3c36c7dda29ea4d93c10d167f147b69a7b22"}, - {file = "pyblish_base-1.8.8-py2.py3-none-any.whl", hash = "sha256:67ea253a05d007ab4a175e44e778928ea7bdb0e9707573e1100417bbf0451a53"}, + {file = "pyblish-base-1.8.11.tar.gz", hash = "sha256:86dfeec0567430eb7eb25f89a18312054147a729ec66f6ac8c7e421fd15b66e1"}, + {file = "pyblish_base-1.8.11-py2.py3-none-any.whl", hash = "sha256:c321be7020c946fe9dfa11941241bd985a572c5009198b4f9810e5afad1f0b4b"}, ] [[package]] @@ -2291,7 +2352,7 @@ files = [ name = "pydocstyle" version = "6.3.0" description = "Python docstring style checker" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2319,14 +2380,14 @@ files = [ [[package]] name = "pygments" -version = "2.14.0" +version = "2.15.0" description = "Pygments is a syntax highlighting package written in Python." -category = "main" +category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, - {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, + {file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"}, + {file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"}, ] [package.extras] @@ -2334,18 +2395,18 @@ plugins = ["importlib-metadata"] [[package]] name = "pylint" -version = "2.15.10" +version = "2.17.2" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "pylint-2.15.10-py3-none-any.whl", hash = "sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e"}, - {file = "pylint-2.15.10.tar.gz", hash = "sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5"}, + {file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"}, + {file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"}, ] [package.dependencies] -astroid = ">=2.12.13,<=2.14.0-dev0" +astroid = ">=2.15.2,<=2.17.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = {version = ">=0.2", markers = "python_version < \"3.11\""} isort = ">=4.2.5,<6" @@ -2719,43 +2780,40 @@ six = ">=1.5" [[package]] name = "python-engineio" -version = "3.14.2" -description = "Engine.IO server" +version = "4.4.0" +description = "Engine.IO server and client for Python" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "python-engineio-3.14.2.tar.gz", hash = "sha256:eab4553f2804c1ce97054c8b22cf0d5a9ab23128075248b97e1a5b2f29553085"}, - {file = "python_engineio-3.14.2-py2.py3-none-any.whl", hash = "sha256:5a9e6086d192463b04a1428ff1f85b6ba631bbb19d453b144ffc04f530542b84"}, + {file = "python-engineio-4.4.0.tar.gz", hash = "sha256:bcc035c70ecc30acc3cfd49ef19aca6c51fa6caaadd0fa58c2d7480f50d04cf2"}, + {file = "python_engineio-4.4.0-py3-none-any.whl", hash = "sha256:11f9c35b775fe70e0a25f67b16d5b69fbfafc368cdd87eeb6f4135a475c88e50"}, ] -[package.dependencies] -six = ">=1.9.0" - [package.extras] asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] [[package]] name = "python-socketio" -version = "4.6.1" -description = "Socket.IO server" +version = "5.8.0" +description = "Socket.IO server and client for Python" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "python-socketio-4.6.1.tar.gz", hash = "sha256:cd1f5aa492c1eb2be77838e837a495f117e17f686029ebc03d62c09e33f4fa10"}, - {file = "python_socketio-4.6.1-py2.py3-none-any.whl", hash = "sha256:5a21da53fdbdc6bb6c8071f40e13d100e0b279ad997681c2492478e06f370523"}, + {file = "python-socketio-5.8.0.tar.gz", hash = "sha256:e714f4dddfaaa0cb0e37a1e2deef2bb60590a5b9fea9c343dd8ca5e688416fd9"}, + {file = "python_socketio-5.8.0-py3-none-any.whl", hash = "sha256:7adb8867aac1c2929b9c1429f1c02e12ca4c36b67c807967393e367dfbb01441"}, ] [package.dependencies] -python-engineio = ">=3.13.0,<4" +bidict = ">=0.21.0" +python-engineio = ">=4.3.0" requests = {version = ">=2.21.0", optional = true, markers = "extra == \"client\""} -six = ">=1.9.0" websocket-client = {version = ">=0.54.0", optional = true, markers = "extra == \"client\""} [package.extras] -asyncio-client = ["aiohttp (>=3.4)", "websockets (>=7.0)"] +asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] [[package]] @@ -2784,18 +2842,6 @@ files = [ {file = "python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8"}, ] -[[package]] -name = "pytz" -version = "2022.7.1" -description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, - {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, -] - [[package]] name = "pywin32" version = "301" @@ -2832,7 +2878,7 @@ files = [ name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2880,16 +2926,19 @@ files = [ [[package]] name = "qt-py" -version = "1.3.7" +version = "1.3.8" description = "Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5." category = "main" optional = false python-versions = "*" files = [ - {file = "Qt.py-1.3.7-py2.py3-none-any.whl", hash = "sha256:150099d1c6f64c9621a2c9d79d45102ec781c30ee30ee69fc082c6e9be7324fe"}, - {file = "Qt.py-1.3.7.tar.gz", hash = "sha256:803c7bdf4d6230f9a466be19d55934a173eabb61406d21cb91e80c2a3f773b1f"}, + {file = "Qt.py-1.3.8-py2.py3-none-any.whl", hash = "sha256:665b9d4cfefaff2d697876d5027e145a0e0b1ba62dda9652ea114db134bc9911"}, + {file = "Qt.py-1.3.8.tar.gz", hash = "sha256:6d330928f7ec8db8e329b19116c52482b6abfaccfa5edef0248e57d012300895"}, ] +[package.dependencies] +types-PySide2 = "*" + [[package]] name = "qtawesome" version = "0.7.3" @@ -2908,14 +2957,14 @@ six = "*" [[package]] name = "qtpy" -version = "2.3.0" +version = "2.3.1" description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "QtPy-2.3.0-py3-none-any.whl", hash = "sha256:8d6d544fc20facd27360ea189592e6135c614785f0dec0b4f083289de6beb408"}, - {file = "QtPy-2.3.0.tar.gz", hash = "sha256:0603c9c83ccc035a4717a12908bf6bc6cb22509827ea2ec0e94c2da7c9ed57c5"}, + {file = "QtPy-2.3.1-py3-none-any.whl", hash = "sha256:5193d20e0b16e4d9d3bc2c642d04d9f4e2c892590bd1b9c92bfe38a95d5a2e12"}, + {file = "QtPy-2.3.1.tar.gz", hash = "sha256:a8c74982d6d172ce124d80cafd39653df78989683f760f2281ba91a6e7b9de8b"}, ] [package.dependencies] @@ -2943,19 +2992,19 @@ sphinx = ">=1.3.1" [[package]] name = "requests" -version = "2.28.1" +version = "2.28.2" description = "Python HTTP for Humans." category = "main" optional = false python-versions = ">=3.7, <4" files = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" @@ -3075,19 +3124,19 @@ files = [ [[package]] name = "slack-sdk" -version = "3.19.5" +version = "3.21.1" description = "The Slack API Platform SDK for Python" category = "main" optional = false python-versions = ">=3.6.0" files = [ - {file = "slack_sdk-3.19.5-py2.py3-none-any.whl", hash = "sha256:0b52bb32a87c71f638b9eb47e228dffeebf89de5e762684ef848276f9f186c84"}, - {file = "slack_sdk-3.19.5.tar.gz", hash = "sha256:47fb4af596243fe6585a92f3034de21eb2104a55cc9fd59a92ef3af17cf9ddd8"}, + {file = "slack_sdk-3.21.1-py2.py3-none-any.whl", hash = "sha256:276358fcddaec49895bea50174e4bd7f83d53b74b917de03ca511e145c6e75d2"}, + {file = "slack_sdk-3.21.1.tar.gz", hash = "sha256:451f2394f6d3696d08c9b290844332aab6e8e39473327fc3f7d19794c7eb441d"}, ] [package.extras] -optional = ["SQLAlchemy (>=1,<2)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"] -testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "codecov (>=2,<3)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"] +optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"] +testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"] [[package]] name = "smmap" @@ -3105,7 +3154,7 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "main" +category = "dev" optional = false python-versions = "*" files = [ @@ -3129,7 +3178,7 @@ files = [ name = "sphinx" version = "5.3.0" description = "Python documentation generator" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3163,21 +3212,21 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-autoapi" -version = "2.0.1" +version = "2.1.0" description = "Sphinx API documentation generator" category = "dev" -optional = true +optional = false python-versions = ">=3.7" files = [ - {file = "sphinx-autoapi-2.0.1.tar.gz", hash = "sha256:cdf47968c20852f4feb0ccefd09e414bb820af8af8f82fab15a24b09a3d1baba"}, - {file = "sphinx_autoapi-2.0.1-py2.py3-none-any.whl", hash = "sha256:8ed197a0c9108770aa442a5445744c1405b356ea64df848e8553411b9b9e129b"}, + {file = "sphinx-autoapi-2.1.0.tar.gz", hash = "sha256:5b5c58064214d5a846c9c81d23f00990a64654b9bca10213231db54a241bc50f"}, + {file = "sphinx_autoapi-2.1.0-py2.py3-none-any.whl", hash = "sha256:b25c7b2cda379447b8c36b6a0e3bdf76e02fd64f7ca99d41c6cbdf130a01768f"}, ] [package.dependencies] astroid = ">=2.7" Jinja2 = "*" PyYAML = "*" -sphinx = ">=4.0" +sphinx = ">=5.2.0" unidecode = "*" [package.extras] @@ -3187,14 +3236,14 @@ go = ["sphinxcontrib-golangdomain"] [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.3" +version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -category = "main" +category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "sphinxcontrib.applehelp-1.0.3-py3-none-any.whl", hash = "sha256:ba0f2a22e6eeada8da6428d0d520215ee8864253f32facf958cca81e426f661d"}, - {file = "sphinxcontrib.applehelp-1.0.3.tar.gz", hash = "sha256:83749f09f6ac843b8cb685277dbc818a8bf2d76cc19602699094fe9a74db529e"}, + {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, + {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, ] [package.extras] @@ -3205,7 +3254,7 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -3219,14 +3268,14 @@ test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.0" +version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "main" +category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, + {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, + {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, ] [package.extras] @@ -3237,7 +3286,7 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "main" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -3253,7 +3302,7 @@ name = "sphinxcontrib-napoleon" version = "0.7" description = "Sphinx \"napoleon\" extension." category = "dev" -optional = true +optional = false python-versions = "*" files = [ {file = "sphinxcontrib-napoleon-0.7.tar.gz", hash = "sha256:407382beed396e9f2d7f3043fad6afda95719204a1e1a231ac865f40abcbfcf8"}, @@ -3268,7 +3317,7 @@ six = ">=1.5.2" name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -3284,7 +3333,7 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -3350,33 +3399,44 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.6" +version = "0.11.7" description = "Style preserving TOML library" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, - {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, + {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"}, + {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, +] + +[[package]] +name = "types-pyside2" +version = "5.15.2.1.3" +description = "The most accurate stubs for PySide2" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "types_PySide2-5.15.2.1.3-py2.py3-none-any.whl", hash = "sha256:a1f3e64d248037f426d3542c1cb693fe64fa783f552efba52347cd1a328fe552"}, ] [[package]] name = "typing-extensions" -version = "4.4.0" +version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, ] [[package]] name = "uc-micro-py" version = "1.0.1" description = "Micro subset of unicode data files for linkify-it-py projects." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3389,14 +3449,14 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "unidecode" -version = "1.2.0" +version = "1.3.6" description = "ASCII transliterations of Unicode text" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" files = [ - {file = "Unidecode-1.2.0-py2.py3-none-any.whl", hash = "sha256:12435ef2fc4cdfd9cf1035a1db7e98b6b047fe591892e81f34e94959591fad00"}, - {file = "Unidecode-1.2.0.tar.gz", hash = "sha256:8d73a97d387a956922344f6b74243c2c6771594659778744b2dbdaad8f6b727d"}, + {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, + {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, ] [[package]] @@ -3413,14 +3473,14 @@ files = [ [[package]] name = "urllib3" -version = "1.26.14" +version = "1.26.15" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, - {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, + {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, + {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, ] [package.extras] @@ -3430,24 +3490,24 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.17.1" +version = "20.21.0" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, - {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, + {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"}, + {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"}, ] [package.dependencies] distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<3" +platformdirs = ">=2.4,<4" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] -testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] [[package]] name = "wcwidth" @@ -3478,91 +3538,102 @@ six = "*" [[package]] name = "wheel" -version = "0.38.4" +version = "0.40.0" description = "A built-package format for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"}, - {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"}, + {file = "wheel-0.40.0-py3-none-any.whl", hash = "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"}, + {file = "wheel-0.40.0.tar.gz", hash = "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873"}, ] [package.extras] -test = ["pytest (>=3.0.0)"] +test = ["pytest (>=6.0.0)"] [[package]] name = "wrapt" -version = "1.14.1" +version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." -category = "main" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, ] [[package]] @@ -3676,21 +3747,24 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.11.0" +version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[extras] +docs = [] [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "6bdb0572a9e255898497ad5ec4d7368d4e0850ce9f4d5c72a37394a2f8f7ec06" +content-hash = "03c6c9ee661cc1de568875033d161478b490203f567d13675e0935dba146ecc8" From ad1453576df79e10686e0a5c13228a8e71d336e2 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 13 Apr 2023 18:03:11 +0200 Subject: [PATCH 218/446] "poetry lock" with --no-update --- poetry.lock | 1103 ++++++++++++++++++++++++--------------------------- 1 file changed, 519 insertions(+), 584 deletions(-) diff --git a/poetry.lock b/poetry.lock index d17e104d69..716ab0adfe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,106 +18,106 @@ resolved_reference = "126f7a188cfe36718f707f42ebbc597e86aa86c3" [[package]] name = "aiohttp" -version = "3.8.4" +version = "3.8.3" description = "Async http client/server framework (asyncio)" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"}, - {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"}, - {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea"}, - {file = "aiohttp-3.8.4-cp310-cp310-win32.whl", hash = "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1"}, - {file = "aiohttp-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24"}, - {file = "aiohttp-3.8.4-cp311-cp311-win32.whl", hash = "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d"}, - {file = "aiohttp-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc"}, - {file = "aiohttp-3.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff"}, - {file = "aiohttp-3.8.4-cp36-cp36m-win32.whl", hash = "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777"}, - {file = "aiohttp-3.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e"}, - {file = "aiohttp-3.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241"}, - {file = "aiohttp-3.8.4-cp37-cp37m-win32.whl", hash = "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a"}, - {file = "aiohttp-3.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d"}, - {file = "aiohttp-3.8.4-cp38-cp38-win32.whl", hash = "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54"}, - {file = "aiohttp-3.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4"}, - {file = "aiohttp-3.8.4-cp39-cp39-win32.whl", hash = "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a"}, - {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"}, - {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"}, + {file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"}, + {file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"}, + {file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"}, + {file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"}, + {file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"}, + {file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"}, + {file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"}, + {file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"}, + {file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"}, + {file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"}, + {file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"}, + {file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"}, + {file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"}, + {file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"}, + {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"}, ] [package.dependencies] aiosignal = ">=1.1.2" async-timeout = ">=4.0.0a3,<5.0" attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<4.0" +charset-normalizer = ">=2.0,<3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" @@ -210,7 +210,7 @@ develop = false type = "git" url = "https://github.com/ActiveState/appdirs.git" reference = "master" -resolved_reference = "8734277956c1df3b85385e6b308e954910533884" +resolved_reference = "211708144ddcbba1f02e26a43efec9aef57bc9fc" [[package]] name = "arrow" @@ -229,19 +229,19 @@ python-dateutil = ">=2.7.0" [[package]] name = "astroid" -version = "2.15.2" +version = "2.13.2" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "astroid-2.15.2-py3-none-any.whl", hash = "sha256:dea89d9f99f491c66ac9c04ebddf91e4acf8bd711722175fe6245c0725cc19bb"}, - {file = "astroid-2.15.2.tar.gz", hash = "sha256:6e61b85c891ec53b07471aec5878f4ac6446a41e590ede0f2ce095f39f7d49dd"}, + {file = "astroid-2.13.2-py3-none-any.whl", hash = "sha256:8f6a8d40c4ad161d6fc419545ae4b2f275ed86d1c989c97825772120842ee0d2"}, + {file = "astroid-2.13.2.tar.gz", hash = "sha256:3bc7834720e1a24ca797fd785d77efb14f7a28ee8e635ef040b6e2d80ccb3303"}, ] [package.dependencies] lazy-object-proxy = ">=1.4.0" -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.0.0" wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} [[package]] @@ -288,14 +288,14 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy [[package]] name = "autopep8" -version = "2.0.2" +version = "2.0.1" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "autopep8-2.0.2-py2.py3-none-any.whl", hash = "sha256:86e9303b5e5c8160872b2f5ef611161b2893e9bfe8ccc7e2f76385947d57a2f1"}, - {file = "autopep8-2.0.2.tar.gz", hash = "sha256:f9849cdd62108cb739dbcdbfb7fdcc9a30d1b63c4cc3e1c1f893b5360941b61c"}, + {file = "autopep8-2.0.1-py2.py3-none-any.whl", hash = "sha256:be5bc98c33515b67475420b7b1feafc8d32c1a69862498eda4983b45bffd2687"}, + {file = "autopep8-2.0.1.tar.gz", hash = "sha256:d27a8929d8dcd21c0f4b3859d2d07c6c25273727b98afc984c039df0f0d86566"}, ] [package.dependencies] @@ -304,14 +304,14 @@ tomli = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "ayon-python-api" -version = "0.1.17" +version = "0.1.16" description = "AYON Python API" category = "main" optional = false python-versions = "*" files = [ - {file = "ayon-python-api-0.1.17.tar.gz", hash = "sha256:f45060f4f8dd26c825823dab00dfc0c3add8309343d823663233f34ca118550a"}, - {file = "ayon_python_api-0.1.17-py3-none-any.whl", hash = "sha256:5d3b11ecca3f30856bb403000009bad2002e72c200cad97b4a9c9d05259c14af"}, + {file = "ayon-python-api-0.1.16.tar.gz", hash = "sha256:666110954dd75b2be1699a29b4732cfb0bcb09d01f64fba4449bfc8ac1fb43f1"}, + {file = "ayon_python_api-0.1.16-py3-none-any.whl", hash = "sha256:bbcd6df1f80ddf32e653a1bb31289cb5fd1a8bea36ab4c8e6aef08c41b6393de"}, ] [package.dependencies] @@ -322,16 +322,19 @@ Unidecode = ">=1.2.0" [[package]] name = "babel" -version = "2.12.1" +version = "2.11.0" description = "Internationalization utilities" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" files = [ - {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, - {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, + {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, + {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, ] +[package.dependencies] +pytz = ">=2015.7" + [[package]] name = "bcrypt" version = "4.0.1" @@ -386,14 +389,14 @@ test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "py [[package]] name = "blessed" -version = "1.20.0" +version = "1.19.1" description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." category = "main" optional = false python-versions = ">=2.7" files = [ - {file = "blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058"}, - {file = "blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"}, + {file = "blessed-1.19.1-py2.py3-none-any.whl", hash = "sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b"}, + {file = "blessed-1.19.1.tar.gz", hash = "sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc"}, ] [package.dependencies] @@ -403,14 +406,14 @@ wcwidth = ">=0.1.4" [[package]] name = "cachetools" -version = "5.3.0" +version = "5.2.1" description = "Extensible memoizing collections and decorators" category = "main" optional = false python-versions = "~=3.7" files = [ - {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"}, - {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"}, + {file = "cachetools-5.2.1-py3-none-any.whl", hash = "sha256:8462eebf3a6c15d25430a8c27c56ac61340b2ecf60c9ce57afc2b97e450e47da"}, + {file = "cachetools-5.2.1.tar.gz", hash = "sha256:5991bc0e08a1319bb618d3195ca5b6bc76646a49c21d55962977197b301cc1fe"}, ] [[package]] @@ -516,89 +519,19 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.1.0" +version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.6.0" files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] +[package.extras] +unicode-backport = ["unicodedata2"] + [[package]] name = "click" version = "7.1.2" @@ -669,63 +602,63 @@ files = [ [[package]] name = "coverage" -version = "7.2.3" +version = "7.0.5" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"}, - {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"}, - {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"}, - {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"}, - {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"}, - {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"}, - {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"}, - {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"}, - {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"}, - {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"}, - {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"}, - {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"}, - {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"}, - {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"}, - {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"}, - {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"}, - {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"}, - {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"}, - {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"}, - {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"}, - {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"}, - {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"}, - {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"}, - {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"}, - {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"}, - {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"}, - {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"}, - {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"}, - {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"}, - {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"}, - {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"}, - {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"}, - {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"}, - {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"}, - {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"}, - {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"}, - {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"}, - {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"}, - {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"}, - {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"}, - {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"}, - {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"}, - {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"}, - {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"}, - {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"}, - {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"}, - {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"}, - {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"}, - {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"}, - {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"}, - {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"}, + {file = "coverage-7.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b"}, + {file = "coverage-7.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89"}, + {file = "coverage-7.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40"}, + {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2"}, + {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289"}, + {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0"}, + {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8"}, + {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809"}, + {file = "coverage-7.0.5-cp310-cp310-win32.whl", hash = "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21"}, + {file = "coverage-7.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad"}, + {file = "coverage-7.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca"}, + {file = "coverage-7.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0"}, + {file = "coverage-7.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196"}, + {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0"}, + {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc"}, + {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45"}, + {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757"}, + {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095"}, + {file = "coverage-7.0.5-cp311-cp311-win32.whl", hash = "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831"}, + {file = "coverage-7.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea"}, + {file = "coverage-7.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c"}, + {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7"}, + {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96"}, + {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029"}, + {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"}, + {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2"}, + {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47"}, + {file = "coverage-7.0.5-cp37-cp37m-win32.whl", hash = "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882"}, + {file = "coverage-7.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d"}, + {file = "coverage-7.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb"}, + {file = "coverage-7.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6"}, + {file = "coverage-7.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd"}, + {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a"}, + {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2"}, + {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7"}, + {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0"}, + {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499"}, + {file = "coverage-7.0.5-cp38-cp38-win32.whl", hash = "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16"}, + {file = "coverage-7.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af"}, + {file = "coverage-7.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab"}, + {file = "coverage-7.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637"}, + {file = "coverage-7.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4"}, + {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded"}, + {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b"}, + {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209"}, + {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78"}, + {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1"}, + {file = "coverage-7.0.5-cp39-cp39-win32.whl", hash = "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904"}, + {file = "coverage-7.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f"}, + {file = "coverage-7.0.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0"}, + {file = "coverage-7.0.5.tar.gz", hash = "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45"}, ] [package.dependencies] @@ -736,45 +669,47 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "40.0.1" +version = "39.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "cryptography-40.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:918cb89086c7d98b1b86b9fdb70c712e5a9325ba6f7d7cfb509e784e0cfc6917"}, - {file = "cryptography-40.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9618a87212cb5200500e304e43691111570e1f10ec3f35569fdfcd17e28fd797"}, - {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4805a4ca729d65570a1b7cac84eac1e431085d40387b7d3bbaa47e39890b88"}, - {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63dac2d25c47f12a7b8aa60e528bfb3c51c5a6c5a9f7c86987909c6c79765554"}, - {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0a4e3406cfed6b1f6d6e87ed243363652b2586b2d917b0609ca4f97072994405"}, - {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1e0af458515d5e4028aad75f3bb3fe7a31e46ad920648cd59b64d3da842e4356"}, - {file = "cryptography-40.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d8aa3609d337ad85e4eb9bb0f8bcf6e4409bfb86e706efa9a027912169e89122"}, - {file = "cryptography-40.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cf91e428c51ef692b82ce786583e214f58392399cf65c341bc7301d096fa3ba2"}, - {file = "cryptography-40.0.1-cp36-abi3-win32.whl", hash = "sha256:650883cc064297ef3676b1db1b7b1df6081794c4ada96fa457253c4cc40f97db"}, - {file = "cryptography-40.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:a805a7bce4a77d51696410005b3e85ae2839bad9aa38894afc0aa99d8e0c3160"}, - {file = "cryptography-40.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd033d74067d8928ef00a6b1327c8ea0452523967ca4463666eeba65ca350d4c"}, - {file = "cryptography-40.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d36bbeb99704aabefdca5aee4eba04455d7a27ceabd16f3b3ba9bdcc31da86c4"}, - {file = "cryptography-40.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:32057d3d0ab7d4453778367ca43e99ddb711770477c4f072a51b3ca69602780a"}, - {file = "cryptography-40.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f5d7b79fa56bc29580faafc2ff736ce05ba31feaa9d4735048b0de7d9ceb2b94"}, - {file = "cryptography-40.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7c872413353c70e0263a9368c4993710070e70ab3e5318d85510cc91cce77e7c"}, - {file = "cryptography-40.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:28d63d75bf7ae4045b10de5413fb1d6338616e79015999ad9cf6fc538f772d41"}, - {file = "cryptography-40.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6f2bbd72f717ce33100e6467572abaedc61f1acb87b8d546001328d7f466b778"}, - {file = "cryptography-40.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cc3a621076d824d75ab1e1e530e66e7e8564e357dd723f2533225d40fe35c60c"}, - {file = "cryptography-40.0.1.tar.gz", hash = "sha256:2803f2f8b1e95f614419926c7e6f55d828afc614ca5ed61543877ae668cc3472"}, + {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"}, + {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"}, + {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"}, + {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"}, + {file = "cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"}, + {file = "cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"}, + {file = "cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"}, ] [package.dependencies] cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "check-manifest", "mypy", "ruff"] +pep8test = ["black", "ruff"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] -tox = ["tox"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] name = "cx-freeze" @@ -945,14 +880,14 @@ stone = ">=2" [[package]] name = "enlighten" -version = "1.11.2" +version = "1.11.1" description = "Enlighten Progress Bar" category = "main" optional = false python-versions = "*" files = [ - {file = "enlighten-1.11.2-py2.py3-none-any.whl", hash = "sha256:98c9eb20e022b6a57f1c8d4f17e16760780b6881e6d658c40f52d21255ea45f3"}, - {file = "enlighten-1.11.2.tar.gz", hash = "sha256:9284861dee5a272e0e1a3758cd3f3b7180b1bd1754875da76876f2a7f46ccb61"}, + {file = "enlighten-1.11.1-py2.py3-none-any.whl", hash = "sha256:e825eb534ca80778bb7d46e5581527b2a6fae559b6cf09e290a7952c6e11961e"}, + {file = "enlighten-1.11.1.tar.gz", hash = "sha256:57abd98a3d3f83484ef9f91f9255f4d23c8b3097ecdb647c7b9b0049d600b7f8"}, ] [package.dependencies] @@ -961,30 +896,30 @@ prefixed = ">=0.3.2" [[package]] name = "evdev" -version = "1.6.1" +version = "1.6.0" description = "Bindings to the Linux input handling subsystem" category = "main" optional = false python-versions = "*" files = [ - {file = "evdev-1.6.1.tar.gz", hash = "sha256:299db8628cc73b237fc1cc57d3c2948faa0756e2a58b6194b5bf81dc2081f1e3"}, + {file = "evdev-1.6.0.tar.gz", hash = "sha256:ecfa01b5c84f7e8c6ced3367ac95288f43cd84efbfd7dd7d0cdbfc0d18c87a6a"}, ] [[package]] name = "filelock" -version = "3.11.0" +version = "3.9.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "filelock-3.11.0-py3-none-any.whl", hash = "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"}, - {file = "filelock-3.11.0.tar.gz", hash = "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37"}, + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, ] [package.extras] -docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -1089,14 +1024,14 @@ files = [ [[package]] name = "ftrack-python-api" -version = "2.4.2" +version = "2.3.3" description = "Python API for ftrack." category = "main" optional = false -python-versions = ">=2.7.9, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=2.7.9, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, < 3.10" files = [ - {file = "ftrack-python-api-2.4.2.tar.gz", hash = "sha256:4bc85ac90421dd70cd0c513534677d40898ec1554104e88267046f6b634bb145"}, - {file = "ftrack_python_api-2.4.2-py2.py3-none-any.whl", hash = "sha256:a8d8cdd9337d83fe4636081dfd0b74cc2552b50531c7fb58efa194b5aeb807bd"}, + {file = "ftrack-python-api-2.3.3.tar.gz", hash = "sha256:358f37e5b1c5635eab107c19e27a0c890d512877f78af35b1ac416e90c037295"}, + {file = "ftrack_python_api-2.3.3-py2.py3-none-any.whl", hash = "sha256:82834c4d5def5557a2ea547a7e6f6ba84d3129e8f90457d8bbd85b287a2c39f6"}, ] [package.dependencies] @@ -1158,14 +1093,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.31" -description = "GitPython is a Python library used to interact with Git repositories" +version = "3.1.30" +description = "GitPython is a python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"}, - {file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"}, + {file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"}, + {file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"}, ] [package.dependencies] @@ -1216,14 +1151,14 @@ uritemplate = ">=3.0.0,<4dev" [[package]] name = "google-auth" -version = "2.17.3" +version = "2.16.0" description = "Google Authentication Library" category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ - {file = "google-auth-2.17.3.tar.gz", hash = "sha256:ce311e2bc58b130fddf316df57c9b3943c2a7b4f6ec31de9663a9333e4064efc"}, - {file = "google_auth-2.17.3-py2.py3-none-any.whl", hash = "sha256:f586b274d3eb7bd932ea424b1c702a30e0393a2e2bc4ca3eae8263ffd8be229f"}, + {file = "google-auth-2.16.0.tar.gz", hash = "sha256:ed7057a101af1146f0554a769930ac9de506aeca4fd5af6543ebe791851a9fbd"}, + {file = "google_auth-2.16.0-py2.py3-none-any.whl", hash = "sha256:5045648c821fb72384cdc0e82cc326df195f113a33049d9b62b74589243d2acc"}, ] [package.dependencies] @@ -1258,14 +1193,14 @@ six = "*" [[package]] name = "googleapis-common-protos" -version = "1.59.0" +version = "1.58.0" description = "Common protobufs used in Google APIs" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.59.0.tar.gz", hash = "sha256:4168fcb568a826a52f23510412da405abd93f4d23ba544bb68d943b14ba3cb44"}, - {file = "googleapis_common_protos-1.59.0-py2.py3-none-any.whl", hash = "sha256:b287dc48449d1d41af0c69f4ea26242b5ae4c3d7249a38b0984c86a4caffff1f"}, + {file = "googleapis-common-protos-1.58.0.tar.gz", hash = "sha256:c727251ec025947d545184ba17e3578840fc3a24a0516a020479edab660457df"}, + {file = "googleapis_common_protos-1.58.0-py2.py3-none-any.whl", hash = "sha256:ca3befcd4580dab6ad49356b46bf165bb68ff4b32389f028f1abd7c10ab9519a"}, ] [package.dependencies] @@ -1276,14 +1211,14 @@ grpc = ["grpcio (>=1.44.0,<2.0.0dev)"] [[package]] name = "httplib2" -version = "0.22.0" +version = "0.21.0" description = "A comprehensive HTTP client library." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, - {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, + {file = "httplib2-0.21.0-py3-none-any.whl", hash = "sha256:987c8bb3eb82d3fa60c68699510a692aa2ad9c4bd4f123e51dfb1488c14cdd01"}, + {file = "httplib2-0.21.0.tar.gz", hash = "sha256:fc144f091c7286b82bec71bdbd9b27323ba709cc612568d3000893bfd9cb4b34"}, ] [package.dependencies] @@ -1291,14 +1226,14 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 [[package]] name = "identify" -version = "2.5.22" +version = "2.5.13" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"}, - {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"}, + {file = "identify-2.5.13-py2.py3-none-any.whl", hash = "sha256:8aa48ce56e38c28b6faa9f261075dea0a942dfbb42b341b4e711896cbb40f3f7"}, + {file = "identify-2.5.13.tar.gz", hash = "sha256:abb546bca6f470228785338a01b539de8a85bbf46491250ae03363956d8ebb10"}, ] [package.extras] @@ -1330,14 +1265,14 @@ files = [ [[package]] name = "importlib-metadata" -version = "6.3.0" +version = "6.0.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_metadata-6.3.0-py3-none-any.whl", hash = "sha256:8f8bd2af397cf33bd344d35cfe7f489219b7d14fc79a3f854b75b8417e9226b0"}, - {file = "importlib_metadata-6.3.0.tar.gz", hash = "sha256:23c2bcae4762dfb0bbe072d358faec24957901d75b6c4ab11172c0c982532402"}, + {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, + {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, ] [package.dependencies] @@ -1362,19 +1297,19 @@ files = [ [[package]] name = "isort" -version = "5.12.0" +version = "5.11.4" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.7.0" files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, + {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, + {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, ] [package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] @@ -1625,24 +1560,24 @@ mistune = "0.8.4" [[package]] name = "markdown-it-py" -version = "2.2.0" +version = "2.1.0" description = "Python port of markdown-it. Markdown parsing, done right!" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, + {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, + {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] +benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] +code-style = ["pre-commit (==2.6)"] +compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] +linkify = ["linkify-it-py (>=1.0,<2.0)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] @@ -1741,14 +1676,14 @@ files = [ [[package]] name = "mdit-py-plugins" -version = "0.3.5" +version = "0.3.3" description = "Collection of plugins for markdown-it-py" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, - {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, + {file = "mdit-py-plugins-0.3.3.tar.gz", hash = "sha256:5cfd7e7ac582a594e23ba6546a2f406e94e42eb33ae596d0734781261c251260"}, + {file = "mdit_py_plugins-0.3.3-py3-none-any.whl", hash = "sha256:36d08a29def19ec43acdcd8ba471d3ebab132e7879d442760d963f19913e04b9"}, ] [package.dependencies] @@ -1986,37 +1921,39 @@ view = ["PySide2 (>=5.11,<6.0)"] [[package]] name = "packaging" -version = "23.1" +version = "23.0" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] [[package]] name = "paramiko" -version = "3.1.0" +version = "2.12.0" description = "SSH2 protocol library" category = "main" optional = false -python-versions = ">=3.6" +python-versions = "*" files = [ - {file = "paramiko-3.1.0-py3-none-any.whl", hash = "sha256:f0caa660e797d9cd10db6fc6ae81e2c9b2767af75c3180fcd0e46158cd368d7f"}, - {file = "paramiko-3.1.0.tar.gz", hash = "sha256:6950faca6819acd3219d4ae694a23c7a87ee38d084f70c1724b0c0dbb8b75769"}, + {file = "paramiko-2.12.0-py2.py3-none-any.whl", hash = "sha256:b2df1a6325f6996ef55a8789d0462f5b502ea83b3c990cbb5bbe57345c6812c4"}, + {file = "paramiko-2.12.0.tar.gz", hash = "sha256:376885c05c5d6aa6e1f4608aac2a6b5b0548b1add40274477324605903d9cd49"}, ] [package.dependencies] -bcrypt = ">=3.2" -cryptography = ">=3.3" -pynacl = ">=1.5" +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" +six = "*" [package.extras] -all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +all = ["bcrypt (>=3.1.3)", "gssapi (>=1.4.1)", "invoke (>=1.3)", "pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["bcrypt (>=3.1.3)", "pynacl (>=1.0.1)"] gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -invoke = ["invoke (>=2.0)"] +invoke = ["invoke (>=1.3)"] [[package]] name = "parso" @@ -2036,19 +1973,18 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "patchelf" -version = "0.17.2.1" +version = "0.17.2.0" description = "A small utility to modify the dynamic linker and RPATH of ELF executables." category = "dev" optional = false python-versions = "*" files = [ - {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:fc329da0e8f628bd836dfb8eaf523547e342351fa8f739bf2b3fe4a6db5a297c"}, - {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ccb266a94edf016efe80151172c26cff8c2ec120a57a1665d257b0442784195d"}, - {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:f47b5bdd6885cfb20abdd14c707d26eb6f499a7f52e911865548d4aa43385502"}, - {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:a9e6ebb0874a11f7ed56d2380bfaa95f00612b23b15f896583da30c2059fcfa8"}, - {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.musllinux_1_1_i686.whl", hash = "sha256:3c8d58f0e4c1929b1c7c45ba8da5a84a8f1aa6a82a46e1cfb2e44a4d40f350e5"}, - {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d1a9bc0d4fd80c038523ebdc451a1cce75237cfcc52dbd1aca224578001d5927"}, - {file = "patchelf-0.17.2.1.tar.gz", hash = "sha256:a6eb0dd452ce4127d0d5e1eb26515e39186fa609364274bc1b0b77539cfa7031"}, + {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b8d86f32e1414d6964d5d166ddd2cf829d156fba0d28d32a3bd0192f987f4529"}, + {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:9233a0f2fc73820c5bd468f27507bdf0c9ac543f07c7f9888bb7cf910b1be22f"}, + {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:6601d7d831508bcdd3d8ebfa6435c2379bf11e41af2409ae7b88de572926841c"}, + {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.musllinux_1_1_i686.whl", hash = "sha256:c62a34f0c25e6c2d6ae44389f819a00ccdf3f292ad1b814fbe1cc23cb27023ce"}, + {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:1b9fd14f300341dc020ae05c49274dd1fa6727eabb4e61dd7fb6fb3600acd26e"}, + {file = "patchelf-0.17.2.0.tar.gz", hash = "sha256:dedf987a83d7f6d6f5512269e57f5feeec36719bd59567173b6d9beabe019efe"}, ] [package.extras] @@ -2071,99 +2007,110 @@ six = "*" [[package]] name = "pillow" -version = "9.5.0" +version = "9.4.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, - {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, - {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, - {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, - {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, - {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, - {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, - {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, - {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, - {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, - {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, - {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, - {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, - {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, - {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, - {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, - {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, + {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, + {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, + {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, + {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, + {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, + {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, + {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, + {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, + {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, + {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, + {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, + {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, + {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, + {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, + {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, + {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, + {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, + {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, + {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, + {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, + {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, + {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, + {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, + {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, + {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, + {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, + {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, + {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, + {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, + {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, + {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, + {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, + {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, + {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, + {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, + {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, + {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, + {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, + {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, + {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, + {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, + {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "platformdirs" -version = "3.2.0" +version = "2.6.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, - {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, ] [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -2210,14 +2157,14 @@ six = ">=1.5.2" [[package]] name = "pre-commit" -version = "3.2.2" +version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"}, - {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"}, + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, ] [package.dependencies] @@ -2229,37 +2176,38 @@ virtualenv = ">=20.10.0" [[package]] name = "prefixed" -version = "0.7.0" +version = "0.6.0" description = "Prefixed alternative numeric library" category = "main" optional = false python-versions = "*" files = [ - {file = "prefixed-0.7.0-py2.py3-none-any.whl", hash = "sha256:537b0e4ff4516c4578f277a41d7104f769d6935ae9cdb0f88fed82ec7b3c0ca5"}, - {file = "prefixed-0.7.0.tar.gz", hash = "sha256:0b54d15e602eb8af4ac31b1db21a37ea95ce5890e0741bb0dd9ded493cefbbe9"}, + {file = "prefixed-0.6.0-py2.py3-none-any.whl", hash = "sha256:5ab094773dc71df68cc78151c81510b9521dcc6b58a4acb78442b127d4e400fa"}, + {file = "prefixed-0.6.0.tar.gz", hash = "sha256:b39fbfac72618fa1eeb5b3fd9ed1341f10dd90df75499cb4c38a6c3ef47cdd94"}, ] [[package]] name = "protobuf" -version = "4.22.3" +version = "4.21.12" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.22.3-cp310-abi3-win32.whl", hash = "sha256:8b54f56d13ae4a3ec140076c9d937221f887c8f64954673d46f63751209e839a"}, - {file = "protobuf-4.22.3-cp310-abi3-win_amd64.whl", hash = "sha256:7760730063329d42a9d4c4573b804289b738d4931e363ffbe684716b796bde51"}, - {file = "protobuf-4.22.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:d14fc1a41d1a1909998e8aff7e80d2a7ae14772c4a70e4bf7db8a36690b54425"}, - {file = "protobuf-4.22.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:70659847ee57a5262a65954538088a1d72dfc3e9882695cab9f0c54ffe71663b"}, - {file = "protobuf-4.22.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:13233ee2b9d3bd9a5f216c1fa2c321cd564b93d8f2e4f521a85b585447747997"}, - {file = "protobuf-4.22.3-cp37-cp37m-win32.whl", hash = "sha256:ecae944c6c2ce50dda6bf76ef5496196aeb1b85acb95df5843cd812615ec4b61"}, - {file = "protobuf-4.22.3-cp37-cp37m-win_amd64.whl", hash = "sha256:d4b66266965598ff4c291416be429cef7989d8fae88b55b62095a2331511b3fa"}, - {file = "protobuf-4.22.3-cp38-cp38-win32.whl", hash = "sha256:f08aa300b67f1c012100d8eb62d47129e53d1150f4469fd78a29fa3cb68c66f2"}, - {file = "protobuf-4.22.3-cp38-cp38-win_amd64.whl", hash = "sha256:f2f4710543abec186aee332d6852ef5ae7ce2e9e807a3da570f36de5a732d88e"}, - {file = "protobuf-4.22.3-cp39-cp39-win32.whl", hash = "sha256:7cf56e31907c532e460bb62010a513408e6cdf5b03fb2611e4b67ed398ad046d"}, - {file = "protobuf-4.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:e0e630d8e6a79f48c557cd1835865b593d0547dce221c66ed1b827de59c66c97"}, - {file = "protobuf-4.22.3-py3-none-any.whl", hash = "sha256:52f0a78141078077cfe15fe333ac3e3a077420b9a3f5d1bf9b5fe9d286b4d881"}, - {file = "protobuf-4.22.3.tar.gz", hash = "sha256:23452f2fdea754a8251d0fc88c0317735ae47217e0d27bf330a30eec2848811a"}, + {file = "protobuf-4.21.12-cp310-abi3-win32.whl", hash = "sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1"}, + {file = "protobuf-4.21.12-cp310-abi3-win_amd64.whl", hash = "sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2"}, + {file = "protobuf-4.21.12-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791"}, + {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97"}, + {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7"}, + {file = "protobuf-4.21.12-cp37-cp37m-win32.whl", hash = "sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717"}, + {file = "protobuf-4.21.12-cp37-cp37m-win_amd64.whl", hash = "sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"}, + {file = "protobuf-4.21.12-cp38-cp38-win32.whl", hash = "sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec"}, + {file = "protobuf-4.21.12-cp38-cp38-win_amd64.whl", hash = "sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30"}, + {file = "protobuf-4.21.12-cp39-cp39-win32.whl", hash = "sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc"}, + {file = "protobuf-4.21.12-cp39-cp39-win_amd64.whl", hash = "sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b"}, + {file = "protobuf-4.21.12-py2.py3-none-any.whl", hash = "sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5"}, + {file = "protobuf-4.21.12-py3-none-any.whl", hash = "sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462"}, + {file = "protobuf-4.21.12.tar.gz", hash = "sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab"}, ] [[package]] @@ -2314,14 +2262,14 @@ pyasn1 = ">=0.4.6,<0.5.0" [[package]] name = "pyblish-base" -version = "1.8.11" +version = "1.8.8" description = "Plug-in driven automation framework for content" category = "main" optional = false python-versions = "*" files = [ - {file = "pyblish-base-1.8.11.tar.gz", hash = "sha256:86dfeec0567430eb7eb25f89a18312054147a729ec66f6ac8c7e421fd15b66e1"}, - {file = "pyblish_base-1.8.11-py2.py3-none-any.whl", hash = "sha256:c321be7020c946fe9dfa11941241bd985a572c5009198b4f9810e5afad1f0b4b"}, + {file = "pyblish-base-1.8.8.tar.gz", hash = "sha256:85a2c034dbb86345bf95018f5b7b3c36c7dda29ea4d93c10d167f147b69a7b22"}, + {file = "pyblish_base-1.8.8-py2.py3-none-any.whl", hash = "sha256:67ea253a05d007ab4a175e44e778928ea7bdb0e9707573e1100417bbf0451a53"}, ] [[package]] @@ -2380,14 +2328,14 @@ files = [ [[package]] name = "pygments" -version = "2.15.0" +version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" files = [ - {file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"}, - {file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"}, + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, ] [package.extras] @@ -2395,18 +2343,18 @@ plugins = ["importlib-metadata"] [[package]] name = "pylint" -version = "2.17.2" +version = "2.15.10" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"}, - {file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"}, + {file = "pylint-2.15.10-py3-none-any.whl", hash = "sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e"}, + {file = "pylint-2.15.10.tar.gz", hash = "sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5"}, ] [package.dependencies] -astroid = ">=2.15.2,<=2.17.0-dev0" +astroid = ">=2.12.13,<=2.14.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = {version = ">=0.2", markers = "python_version < \"3.11\""} isort = ">=4.2.5,<6" @@ -2842,6 +2790,18 @@ files = [ {file = "python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8"}, ] +[[package]] +name = "pytz" +version = "2022.7.1" +description = "World timezone definitions, modern and historical" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, + {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, +] + [[package]] name = "pywin32" version = "301" @@ -2926,19 +2886,16 @@ files = [ [[package]] name = "qt-py" -version = "1.3.8" +version = "1.3.7" description = "Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5." category = "main" optional = false python-versions = "*" files = [ - {file = "Qt.py-1.3.8-py2.py3-none-any.whl", hash = "sha256:665b9d4cfefaff2d697876d5027e145a0e0b1ba62dda9652ea114db134bc9911"}, - {file = "Qt.py-1.3.8.tar.gz", hash = "sha256:6d330928f7ec8db8e329b19116c52482b6abfaccfa5edef0248e57d012300895"}, + {file = "Qt.py-1.3.7-py2.py3-none-any.whl", hash = "sha256:150099d1c6f64c9621a2c9d79d45102ec781c30ee30ee69fc082c6e9be7324fe"}, + {file = "Qt.py-1.3.7.tar.gz", hash = "sha256:803c7bdf4d6230f9a466be19d55934a173eabb61406d21cb91e80c2a3f773b1f"}, ] -[package.dependencies] -types-PySide2 = "*" - [[package]] name = "qtawesome" version = "0.7.3" @@ -2957,14 +2914,14 @@ six = "*" [[package]] name = "qtpy" -version = "2.3.1" +version = "2.3.0" description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "QtPy-2.3.1-py3-none-any.whl", hash = "sha256:5193d20e0b16e4d9d3bc2c642d04d9f4e2c892590bd1b9c92bfe38a95d5a2e12"}, - {file = "QtPy-2.3.1.tar.gz", hash = "sha256:a8c74982d6d172ce124d80cafd39653df78989683f760f2281ba91a6e7b9de8b"}, + {file = "QtPy-2.3.0-py3-none-any.whl", hash = "sha256:8d6d544fc20facd27360ea189592e6135c614785f0dec0b4f083289de6beb408"}, + {file = "QtPy-2.3.0.tar.gz", hash = "sha256:0603c9c83ccc035a4717a12908bf6bc6cb22509827ea2ec0e94c2da7c9ed57c5"}, ] [package.dependencies] @@ -2992,19 +2949,19 @@ sphinx = ">=1.3.1" [[package]] name = "requests" -version = "2.28.2" +version = "2.28.1" description = "Python HTTP for Humans." category = "main" optional = false python-versions = ">=3.7, <4" files = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset-normalizer = ">=2,<3" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" @@ -3124,19 +3081,19 @@ files = [ [[package]] name = "slack-sdk" -version = "3.21.1" +version = "3.19.5" description = "The Slack API Platform SDK for Python" category = "main" optional = false python-versions = ">=3.6.0" files = [ - {file = "slack_sdk-3.21.1-py2.py3-none-any.whl", hash = "sha256:276358fcddaec49895bea50174e4bd7f83d53b74b917de03ca511e145c6e75d2"}, - {file = "slack_sdk-3.21.1.tar.gz", hash = "sha256:451f2394f6d3696d08c9b290844332aab6e8e39473327fc3f7d19794c7eb441d"}, + {file = "slack_sdk-3.19.5-py2.py3-none-any.whl", hash = "sha256:0b52bb32a87c71f638b9eb47e228dffeebf89de5e762684ef848276f9f186c84"}, + {file = "slack_sdk-3.19.5.tar.gz", hash = "sha256:47fb4af596243fe6585a92f3034de21eb2104a55cc9fd59a92ef3af17cf9ddd8"}, ] [package.extras] -optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"] -testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"] +optional = ["SQLAlchemy (>=1,<2)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"] +testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "codecov (>=2,<3)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"] [[package]] name = "smmap" @@ -3212,21 +3169,21 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-autoapi" -version = "2.1.0" +version = "2.0.1" description = "Sphinx API documentation generator" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "sphinx-autoapi-2.1.0.tar.gz", hash = "sha256:5b5c58064214d5a846c9c81d23f00990a64654b9bca10213231db54a241bc50f"}, - {file = "sphinx_autoapi-2.1.0-py2.py3-none-any.whl", hash = "sha256:b25c7b2cda379447b8c36b6a0e3bdf76e02fd64f7ca99d41c6cbdf130a01768f"}, + {file = "sphinx-autoapi-2.0.1.tar.gz", hash = "sha256:cdf47968c20852f4feb0ccefd09e414bb820af8af8f82fab15a24b09a3d1baba"}, + {file = "sphinx_autoapi-2.0.1-py2.py3-none-any.whl", hash = "sha256:8ed197a0c9108770aa442a5445744c1405b356ea64df848e8553411b9b9e129b"}, ] [package.dependencies] astroid = ">=2.7" Jinja2 = "*" PyYAML = "*" -sphinx = ">=5.2.0" +sphinx = ">=4.0" unidecode = "*" [package.extras] @@ -3236,14 +3193,14 @@ go = ["sphinxcontrib-golangdomain"] [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.4" +version = "1.0.3" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, + {file = "sphinxcontrib.applehelp-1.0.3-py3-none-any.whl", hash = "sha256:ba0f2a22e6eeada8da6428d0d520215ee8864253f32facf958cca81e426f661d"}, + {file = "sphinxcontrib.applehelp-1.0.3.tar.gz", hash = "sha256:83749f09f6ac843b8cb685277dbc818a8bf2d76cc19602699094fe9a74db529e"}, ] [package.extras] @@ -3268,14 +3225,14 @@ test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.1" +version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.6" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, ] [package.extras] @@ -3399,37 +3356,26 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.7" +version = "0.11.6" description = "Style preserving TOML library" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" files = [ - {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"}, - {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, -] - -[[package]] -name = "types-pyside2" -version = "5.15.2.1.3" -description = "The most accurate stubs for PySide2" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "types_PySide2-5.15.2.1.3-py2.py3-none-any.whl", hash = "sha256:a1f3e64d248037f426d3542c1cb693fe64fa783f552efba52347cd1a328fe552"}, + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, ] [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] [[package]] @@ -3449,14 +3395,14 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "unidecode" -version = "1.3.6" +version = "1.2.0" description = "ASCII transliterations of Unicode text" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, - {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, + {file = "Unidecode-1.2.0-py2.py3-none-any.whl", hash = "sha256:12435ef2fc4cdfd9cf1035a1db7e98b6b047fe591892e81f34e94959591fad00"}, + {file = "Unidecode-1.2.0.tar.gz", hash = "sha256:8d73a97d387a956922344f6b74243c2c6771594659778744b2dbdaad8f6b727d"}, ] [[package]] @@ -3473,14 +3419,14 @@ files = [ [[package]] name = "urllib3" -version = "1.26.15" +version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, - {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, ] [package.extras] @@ -3490,24 +3436,24 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.21.0" +version = "20.17.1" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" files = [ - {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"}, - {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"}, + {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, + {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, ] [package.dependencies] distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<4" +platformdirs = ">=2.4,<3" [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] name = "wcwidth" @@ -3538,102 +3484,91 @@ six = "*" [[package]] name = "wheel" -version = "0.40.0" +version = "0.38.4" description = "A built-package format for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "wheel-0.40.0-py3-none-any.whl", hash = "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"}, - {file = "wheel-0.40.0.tar.gz", hash = "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873"}, + {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"}, + {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"}, ] [package.extras] -test = ["pytest (>=6.0.0)"] +test = ["pytest (>=3.0.0)"] [[package]] name = "wrapt" -version = "1.15.0" +version = "1.14.1" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] [[package]] @@ -3747,19 +3682,19 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.15.0" +version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, + {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] docs = [] From facf4fdc65f083f25e774d3b7847d399f2dd80d2 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 12 May 2023 12:24:27 +1000 Subject: [PATCH 219/446] Global: Move PyOpenColorIO to vendor/python so that DCCs don't conflict with their own --- pyproject.toml | 6 ++++- tools/fetch_thirdparty_libs.py | 40 ++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0cb7fb010b..bcd27bd2b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" ayon-python-api = "^0.1" -opencolorio = "^2.2.0" Unidecode = "^1.2" [tool.poetry.dev-dependencies] @@ -131,6 +130,11 @@ version = "6.4.3" package = "PySide2" version = "5.15.2" +# DCC packages supply their own opencolorio, lets not interfere with theirs +[openpype.opencolorio] +package = "opencolorio" +version = "2.2.1" + # TODO: we will need to handle different linux flavours here and # also different macos versions too. [openpype.thirdparty.ffmpeg.windows] diff --git a/tools/fetch_thirdparty_libs.py b/tools/fetch_thirdparty_libs.py index 70257caa46..f06a74b292 100644 --- a/tools/fetch_thirdparty_libs.py +++ b/tools/fetch_thirdparty_libs.py @@ -67,40 +67,43 @@ def _print(msg: str, message_type: int = 0) -> None: print(f"{header}{msg}") -def install_qtbinding(pyproject, openpype_root, platform_name): - _print("Handling Qt binding framework ...") - qtbinding_def = pyproject["openpype"]["qtbinding"][platform_name] - package = qtbinding_def["package"] - version = qtbinding_def.get("version") - - qtbinding_arg = None +def _pip_install(openpype_root, package, version=None): + arg = None if package and version: - qtbinding_arg = f"{package}=={version}" + arg = f"{package}=={version}" elif package: - qtbinding_arg = package + arg = package - if not qtbinding_arg: - _print("Didn't find Qt binding to install") + if not arg: + _print("Couldn't find package to install") sys.exit(1) - _print(f"We'll install {qtbinding_arg}") + _print(f"We'll install {arg}") python_vendor_dir = openpype_root / "vendor" / "python" try: subprocess.run( [ sys.executable, - "-m", "pip", "install", "--upgrade", qtbinding_arg, + "-m", "pip", "install", "--upgrade", arg, "-t", str(python_vendor_dir) ], check=True, stdout=subprocess.DEVNULL ) except subprocess.CalledProcessError as e: - _print("Error during PySide2 installation.", 1) + _print(f"Error during {package} installation.", 1) _print(str(e), 1) sys.exit(1) + +def install_qtbinding(pyproject, openpype_root, platform_name): + _print("Handling Qt binding framework ...") + qtbinding_def = pyproject["openpype"]["qtbinding"][platform_name] + package = qtbinding_def["package"] + version = qtbinding_def.get("version") + _pip_install(openpype_root, package, version) + # Remove libraries for QtSql which don't have available libraries # by default and Postgre library would require to modify rpath of # dependency @@ -112,6 +115,14 @@ def install_qtbinding(pyproject, openpype_root, platform_name): os.remove(str(filepath)) +def install_opencolorio(pyproject, openpype_root): + _print("Installing PyOpenColorIO") + opencolorio_def = pyproject["openpype"]["opencolorio"] + package = opencolorio_def["package"] + version = opencolorio_def.get("version") + _pip_install(openpype_root, package, version) + + def install_thirdparty(pyproject, openpype_root, platform_name): _print("Processing third-party dependencies ...") try: @@ -221,6 +232,7 @@ def main(): pyproject = toml.load(openpype_root / "pyproject.toml") platform_name = platform.system().lower() install_qtbinding(pyproject, openpype_root, platform_name) + install_opencolorio(pyproject, openpype_root) install_thirdparty(pyproject, openpype_root, platform_name) end_time = time.time_ns() total_time = (end_time - start_time) / 1000000000 From 855565b8c976c031be78f9b66e2523911b2c2fcd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 May 2023 13:58:30 +0200 Subject: [PATCH 220/446] Re-remove ayon api from poetry lock (#4964) --- poetry.lock | 18 ------------------ pyproject.toml | 1 - 2 files changed, 19 deletions(-) diff --git a/poetry.lock b/poetry.lock index 716ab0adfe..f96bc71fd5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -302,24 +302,6 @@ files = [ pycodestyle = ">=2.10.0" tomli = {version = "*", markers = "python_version < \"3.11\""} -[[package]] -name = "ayon-python-api" -version = "0.1.16" -description = "AYON Python API" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "ayon-python-api-0.1.16.tar.gz", hash = "sha256:666110954dd75b2be1699a29b4732cfb0bcb09d01f64fba4449bfc8ac1fb43f1"}, - {file = "ayon_python_api-0.1.16-py3-none-any.whl", hash = "sha256:bbcd6df1f80ddf32e653a1bb31289cb5fd1a8bea36ab4c8e6aef08c41b6393de"}, -] - -[package.dependencies] -appdirs = ">=1,<2" -requests = ">=2.27.1" -six = ">=1.15" -Unidecode = ">=1.2.0" - [[package]] name = "babel" version = "2.11.0" diff --git a/pyproject.toml b/pyproject.toml index bcd27bd2b6..65a4b8aada 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,6 @@ requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" -ayon-python-api = "^0.1" Unidecode = "^1.2" [tool.poetry.dev-dependencies] From f6149c30e515e8ddb069b63d0be1a7c739c1bd31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 May 2023 14:59:48 +0200 Subject: [PATCH 221/446] fix ftrack system settings conversion (#4967) --- openpype/settings/ayon_settings.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 30efd61fe4..78cf1284e7 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -166,16 +166,13 @@ def _convert_kitsu_system_settings(ayon_settings, output): } -def _convert_ftrack_system_settings(ayon_settings, output): - ayon_ftrack = ayon_settings["ftrack"] - # Ignore if new ftrack addon is used - if "service_settings" in ayon_ftrack: - output["ftrack"] = ayon_ftrack - return - - output["modules"]["ftrack"] = { - "ftrack_server": ayon_ftrack["ftrack_server"] - } +def _convert_ftrack_system_settings(ayon_settings, output, defaults): + # Ftrack contains few keys that are needed for initialization in OpenPype + # mode and some are used on different places + ftrack_settings = defaults["modules"]["ftrack"] + ftrack_settings["ftrack_server"] = ( + ayon_settings["ftrack"]["ftrack_server"]) + output["modules"]["ftrack"] = ftrack_settings def _convert_shotgrid_system_settings(ayon_settings, output): @@ -257,7 +254,6 @@ def _convert_modules_system( # TODO add 'enabled' values for key, func in ( ("kitsu", _convert_kitsu_system_settings), - ("ftrack", _convert_ftrack_system_settings), ("shotgrid", _convert_shotgrid_system_settings), ("timers_manager", _convert_timers_manager_system_settings), ("clockify", _convert_clockify_system_settings), @@ -268,6 +264,10 @@ def _convert_modules_system( if key in ayon_settings: func(ayon_settings, output) + if "ftrack" in ayon_settings: + _convert_ftrack_system_settings( + ayon_settings, output, default_settings) + output_modules = output["modules"] # TODO remove when not needed for module_name, value in default_settings["modules"].items(): From 1ff18dc4f3fa34838e52b8cf5f91699d2d925f47 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 May 2023 11:53:12 +0200 Subject: [PATCH 222/446] AYON: Linux & MacOS launch script (#4970) * added shell script to launch tray in ayon mode * change variables used in powershell script * change permissions of script file * fix python script filename --- tools/run_tray_ayon.ps1 | 14 ++++---- tools/run_tray_ayon.sh | 78 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 7 deletions(-) create mode 100755 tools/run_tray_ayon.sh diff --git a/tools/run_tray_ayon.ps1 b/tools/run_tray_ayon.ps1 index c0651bdbbe..54a80f93fd 100644 --- a/tools/run_tray_ayon.ps1 +++ b/tools/run_tray_ayon.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Helper script OpenPype Tray. + Helper script AYON Tray. .DESCRIPTION @@ -12,30 +12,30 @@ PS> .\run_tray.ps1 #> $current_dir = Get-Location $script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent -$openpype_root = (Get-Item $script_dir).parent.FullName +$ayon_root = (Get-Item $script_dir).parent.FullName # Install PSWriteColor to support colorized output to terminal -$env:PSModulePath = $env:PSModulePath + ";$($openpype_root)\tools\modules\powershell" +$env:PSModulePath = $env:PSModulePath + ";$($ayon_root)\tools\modules\powershell" $env:_INSIDE_OPENPYPE_TOOL = "1" # make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { - $env:POETRY_HOME = "$openpype_root\.poetry" + $env:POETRY_HOME = "$ayon_root\.poetry" } $env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" -Set-Location -Path $openpype_root +Set-Location -Path $ayon_root Write-Color -Text ">>> ", "Reading Poetry ... " -Color Green, Gray -NoNewline if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Color -Text "NOT FOUND" -Color Yellow Write-Color -Text "*** ", "We need to install Poetry create virtual env first ..." -Color Yellow, Gray - & "$openpype_root\tools\create_env.ps1" + & "$ayon_root\tools\create_env.ps1" } else { Write-Color -Text "OK" -Color Green } -& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\ayon_start.py" tray --debug +& "$($env:POETRY_HOME)\bin\poetry" run python "$($ayon_root)\ayon_start.py" tray --debug Set-Location -Path $current_dir diff --git a/tools/run_tray_ayon.sh b/tools/run_tray_ayon.sh new file mode 100755 index 0000000000..3039750b87 --- /dev/null +++ b/tools/run_tray_ayon.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Run AYON Tray + +# Colors for terminal + +RST='\033[0m' # Text Reset + +# Regular Colors +Black='\033[0;30m' # Black +Red='\033[0;31m' # Red +Green='\033[0;32m' # Green +Yellow='\033[0;33m' # Yellow +Blue='\033[0;34m' # Blue +Purple='\033[0;35m' # Purple +Cyan='\033[0;36m' # Cyan +White='\033[0;37m' # White + +# Bold +BBlack='\033[1;30m' # Black +BRed='\033[1;31m' # Red +BGreen='\033[1;32m' # Green +BYellow='\033[1;33m' # Yellow +BBlue='\033[1;34m' # Blue +BPurple='\033[1;35m' # Purple +BCyan='\033[1;36m' # Cyan +BWhite='\033[1;37m' # White + +# Bold High Intensity +BIBlack='\033[1;90m' # Black +BIRed='\033[1;91m' # Red +BIGreen='\033[1;92m' # Green +BIYellow='\033[1;93m' # Yellow +BIBlue='\033[1;94m' # Blue +BIPurple='\033[1;95m' # Purple +BICyan='\033[1;96m' # Cyan +BIWhite='\033[1;97m' # White + + +############################################################################## +# Return absolute path +# Globals: +# None +# Arguments: +# Path to resolve +# Returns: +# None +############################################################################### +realpath () { + echo $(cd $(dirname "$1"); pwd)/$(basename "$1") +} + +# Main +main () { + # Directories + ayon_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) + + _inside_openpype_tool="1" + + if [[ -z $POETRY_HOME ]]; then + export POETRY_HOME="$ayon_root/.poetry" + fi + + echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" + if [ -f "$POETRY_HOME/bin/poetry" ]; then + echo -e "${BIGreen}OK${RST}" + else + echo -e "${BIYellow}NOT FOUND${RST}" + echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..." + . "$ayon_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; } + fi + + pushd "$ayon_root" > /dev/null || return > /dev/null + + echo -e "${BIGreen}>>>${RST} Running AYON Tray with debug option ..." + "$POETRY_HOME/bin/poetry" run python3 "$ayon_root/ayon_start.py" tray --debug +} + +main From c360d0b28d3bd90e794e32a079fceb06d576165e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 May 2023 12:46:57 +0200 Subject: [PATCH 223/446] AYON: General fixes and updates (#4975) * fix color conversion of maya load colors * this seems to have been accidentally switched before, if there are no versions, return nothing, if there are, return something * maya failed to run the userSetup due to the variable having been changed to settings * fix representation fields conversion * fix missing legacy io * fix tools env conversion * Updated AYON python api --------- Co-authored-by: Sveinbjorn J. Tryggvason --- openpype/client/server/conversion_utils.py | 8 +++++++- openpype/client/server/entities.py | 4 ++-- openpype/hosts/maya/startup/userSetup.py | 2 +- openpype/settings/ayon_settings.py | 4 ++++ .../python/common/ayon_api/entity_hub.py | 12 ++++++++++- .../python/common/ayon_api/graphql_queries.py | 3 +-- .../python/common/ayon_api/server_api.py | 20 ++++++++----------- .../vendor/python/common/ayon_api/version.py | 2 +- 8 files changed, 35 insertions(+), 20 deletions(-) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index e8c2ee9c3c..78098dd9ba 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -177,6 +177,9 @@ def convert_v4_project_to_v3(project): for app_name in apps_attr ] data.update(attribs) + if "tools" in data: + data["tools_env"] = data.pop("tools") + data["entityType"] = "Project" config = {} @@ -357,6 +360,9 @@ def convert_v4_folder_to_v3(folder, project_name): if "attrib" in folder: output_data.update(folder["attrib"]) + if "tools" in output_data: + output_data["tools_env"] = output_data.pop("tools") + if "tasks" in folder: output_data["tasks"] = convert_v4_tasks_to_v3(folder["tasks"]) @@ -600,7 +606,7 @@ def representation_fields_v3_to_v4(fields, con): output |= REPRESENTATION_FILES_FIELDS elif field.startswith("data"): - fields |= { + output |= { "attrib.{}".format(attr) for attr in representation_attributes } diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index e9bb2287a0..2a2662b327 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -391,8 +391,8 @@ def get_last_version_by_subset_id(project_name, subset_id, fields=None): fields=fields ) if not versions: - return versions[0] - return None + return None + return versions[0] def get_last_version_by_subset_name( diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index 595ea7880d..f2899cdb37 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -16,7 +16,7 @@ project_name = get_current_project_name() settings = get_project_settings(project_name) # Loading plugins explicitly. -explicit_plugins_loading = project_settings["maya"]["explicit_plugins_loading"] +explicit_plugins_loading = settings["maya"]["explicit_plugins_loading"] if explicit_plugins_loading["enabled"]: def _explicit_load_plugins(): for plugin in explicit_plugins_loading["plugins_to_load"]: diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 78cf1284e7..ea2b72e580 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -539,6 +539,10 @@ def _convert_maya_project_settings(ayon_settings, output): _convert_host_imageio(ayon_maya) + load_colors = ayon_maya["load"]["colors"] + for key, color in tuple(load_colors.items()): + load_colors[key] = _convert_color(color) + output["maya"] = ayon_maya diff --git a/openpype/vendor/python/common/ayon_api/entity_hub.py b/openpype/vendor/python/common/ayon_api/entity_hub.py index 36489b6439..c6baceeb31 100644 --- a/openpype/vendor/python/common/ayon_api/entity_hub.py +++ b/openpype/vendor/python/common/ayon_api/entity_hub.py @@ -589,12 +589,15 @@ class EntityHub(object): parent_id = task["folderId"] tasks_by_parent_id[parent_id].append(task) + lock_queue = collections.deque() hierarchy_queue = collections.deque() hierarchy_queue.append((None, project_entity)) while hierarchy_queue: item = hierarchy_queue.popleft() parent_id, parent_entity = item + lock_queue.append(parent_entity) + children_ids = set() for folder in folders_by_parent_id[parent_id]: folder_entity = self.add_folder(folder) @@ -604,10 +607,16 @@ class EntityHub(object): for task in tasks_by_parent_id[parent_id]: task_entity = self.add_task(task) + lock_queue.append(task_entity) children_ids.add(task_entity.id) parent_entity.fill_children_ids(children_ids) - self.lock() + + # Lock entities when all are added to hub + # - lock only entities added in this method + while lock_queue: + entity = lock_queue.popleft() + entity.lock() def lock(self): if self._project_entity is None: @@ -1198,6 +1207,7 @@ class BaseEntity(object): self._attribs.lock() self._immutable_for_hierarchy_cache = None + self._created = False def _get_entity_by_id(self, entity_id): return self._entity_hub.get_entity_by_id(entity_id) diff --git a/openpype/vendor/python/common/ayon_api/graphql_queries.py b/openpype/vendor/python/common/ayon_api/graphql_queries.py index 4df377ea18..1fc653cf68 100644 --- a/openpype/vendor/python/common/ayon_api/graphql_queries.py +++ b/openpype/vendor/python/common/ayon_api/graphql_queries.py @@ -314,7 +314,6 @@ def representations_graphql_query(fields): def representations_parents_qraphql_query( version_fields, subset_fields, folder_fields ): - query = GraphQlQuery("RepresentationsParentsQuery") project_name_var = query.add_variable("projectName", "String!") @@ -388,7 +387,7 @@ def workfiles_info_graphql_query(fields): def events_graphql_query(fields): - query = GraphQlQuery("WorkfilesInfo") + query = GraphQlQuery("Events") topics_var = query.add_variable("eventTopics", "[String!]") projects_var = query.add_variable("projectNames", "[String!]") states_var = query.add_variable("eventStates", "[String!]") diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index 796ec13d41..675f5ea4be 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -14,6 +14,7 @@ except ImportError: HTTPStatus = None import requests +from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError from .constants import ( DEFAULT_PROJECT_FIELDS, @@ -112,9 +113,9 @@ class RestApiResponse(object): @property def data(self): if self._data is None: - if self.status != 204: + try: self._data = self.orig_response.json() - else: + except RequestsJSONDecodeError: self._data = {} return self._data @@ -128,7 +129,10 @@ class RestApiResponse(object): @property def detail(self): - return self.get("detail", _get_description(self)) + detail = self.get("detail") + if detail: + return detail + return _get_description(self) @property def status_code(self): @@ -299,14 +303,6 @@ class ServerAPI(object): 'production'). """ - _entity_types_link_mapping = { - "folder": ("folderIds", "folders"), - "task": ("taskIds", "tasks"), - "subset": ("subsetIds", "subsets"), - "version": ("versionIds", "versions"), - "representation": ("representationIds", "representations"), - } - def __init__( self, base_url, @@ -916,7 +912,7 @@ class ServerAPI(object): project_names = set(project_names) if not project_names: return - filters["projectName"] = list(project_names) + filters["projectNames"] = list(project_names) if states is not None: states = set(states) diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index 3120942636..9b38175335 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.1.17-1" +__version__ = "0.1.18" From 4b980b5b1b1a292e8aa548828d4daf6405ee8d63 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 May 2023 17:26:55 +0200 Subject: [PATCH 224/446] General: Removed unused cli commands (#4902) * removed texturecopy and launch arguments from cli commands * removed launch from documentation --- openpype/cli.py | 79 ------------------------- openpype/pype_commands.py | 6 -- website/docs/admin_openpype_commands.md | 21 ------- 3 files changed, 106 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index 54af42920d..17f340bee5 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -200,85 +200,6 @@ def remotepublish(project, path, user=None, targets=None): PypeCommands.remotepublish(project, path, user, targets=targets) -@main.command() -@click.option("-p", "--project", required=True, - help="name of project asset is under") -@click.option("-a", "--asset", required=True, - help="name of asset to which we want to copy textures") -@click.option("--path", required=True, - help="path where textures are found", - type=click.Path(exists=True)) -def texturecopy(project, asset, path): - """Copy specified textures to provided asset path. - - It validates if project and asset exists. Then it will use speedcopy to - copy all textures found in all directories under --path to destination - folder, determined by template texture in anatomy. I will use source - filename and automatically rise version number on directory. - - Result will be copied without directory structure so it will be flat then. - Nothing is written to database. - """ - - PypeCommands().texture_copy(project, asset, path) - - -@main.command(context_settings={"ignore_unknown_options": True}) -@click.option("--app", help="Registered application name") -@click.option("--project", help="Project name", - default=lambda: os.environ.get('AVALON_PROJECT', '')) -@click.option("--asset", help="Asset name", - default=lambda: os.environ.get('AVALON_ASSET', '')) -@click.option("--task", help="Task name", - default=lambda: os.environ.get('AVALON_TASK', '')) -@click.option("--tools", help="List of tools to add") -@click.option("--user", help="Pype user name", - default=lambda: os.environ.get('OPENPYPE_USERNAME', '')) -@click.option("-fs", - "--ftrack-server", - help="Registered application name", - default=lambda: os.environ.get('FTRACK_SERVER', '')) -@click.option("-fu", - "--ftrack-user", - help="Registered application name", - default=lambda: os.environ.get('FTRACK_API_USER', '')) -@click.option("-fk", - "--ftrack-key", - help="Registered application name", - default=lambda: os.environ.get('FTRACK_API_KEY', '')) -@click.argument('arguments', nargs=-1) -def launch(app, project, asset, task, - ftrack_server, ftrack_user, ftrack_key, tools, arguments, user): - """Launch registered application name in Pype context. - - You can define applications in pype-config toml files. Project, asset name - and task name must be provided (even if they are not used by app itself). - Optionally you can specify ftrack credentials if needed. - - ARGUMENTS are passed to launched application. - - """ - # TODO: this needs to switch for Settings - if ftrack_server: - os.environ["FTRACK_SERVER"] = ftrack_server - - if ftrack_server: - os.environ["FTRACK_API_USER"] = ftrack_user - - if ftrack_server: - os.environ["FTRACK_API_KEY"] = ftrack_key - - if user: - os.environ["OPENPYPE_USERNAME"] = user - - # test required - if not project or not asset or not task: - print("!!! Missing required arguments") - return - - PypeCommands().run_application(app, project, asset, task, tools, arguments) - - @main.command(context_settings={"ignore_unknown_options": True}) def projectmanager(): PypeCommands().launch_project_manager() diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 10f755ae77..8a3f25a026 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -265,12 +265,6 @@ class PypeCommands: main(output_path, project_name, asset_name, strict) - def texture_copy(self, project, asset, path): - pass - - def run_application(self, app, project, asset, task, tools, arguments): - pass - def validate_jsons(self): pass diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index 131b6c0a51..a149d78aa2 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -40,7 +40,6 @@ For more information [see here](admin_use.md#run-openpype). | module | Run command line arguments for modules. | | | repack-version | Tool to re-create version zip. | [📑](#repack-version-arguments) | | tray | Launch OpenPype Tray. | [📑](#tray-arguments) -| launch | Launch application in Pype environment. | [📑](#launch-arguments) | | publish | Pype takes JSON from provided path and use it to publish data in it. | [📑](#publish-arguments) | | extractenvironments | Extract environment variables for entered context to a json file. | [📑](#extractenvironments-arguments) | | run | Execute given python script within OpenPype environment. | [📑](#run-arguments) | @@ -54,26 +53,6 @@ For more information [see here](admin_use.md#run-openpype). ```shell openpype_console tray ``` ---- - -### `launch` arguments {#launch-arguments} - -| Argument | Description | -| --- | --- | -| `--app` | Application name - this should be the key for application from Settings. | -| `--project` | Project name (default taken from `AVALON_PROJECT` if set) | -| `--asset` | Asset name (default taken from `AVALON_ASSET` if set) | -| `--task` | Task name (default taken from `AVALON_TASK` is set) | -| `--tools` | *Optional: Additional tools to add* | -| `--user` | *Optional: User on behalf to run* | -| `--ftrack-server` / `-fs` | *Optional: Ftrack server URL* | -| `--ftrack-user` / `-fu` | *Optional: Ftrack user* | -| `--ftrack-key` / `-fk` | *Optional: Ftrack API key* | - -For example to run Python interactive console in Pype context: -```shell -pype launch --app python --project my_project --asset my_asset --task my_task -``` --- ### `publish` arguments {#publish-arguments} From 0daa5580d974e0f6bc65151139351bc175c43558 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 May 2023 17:31:00 +0200 Subject: [PATCH 225/446] AYON: ISO date format conversion issues (#4981) * added arrow to requirements of openpype * moved 'arrow' from ftrack python 2 dependencies to global * use arrow in conversion utils for datetime conversion * skip datetime conversion --- openpype/client/server/conversion_utils.py | 4 +- openpype/modules/ftrack/ftrack_module.py | 2 - .../ftrack/python2_vendor/arrow/.gitignore | 211 -- .../arrow/.pre-commit-config.yaml | 41 - .../ftrack/python2_vendor/arrow/CHANGELOG.rst | 598 ----- .../ftrack/python2_vendor/arrow/LICENSE | 201 -- .../ftrack/python2_vendor/arrow/MANIFEST.in | 3 - .../ftrack/python2_vendor/arrow/Makefile | 44 - .../ftrack/python2_vendor/arrow/README.rst | 133 - .../ftrack/python2_vendor/arrow/docs/Makefile | 20 - .../ftrack/python2_vendor/arrow/docs/conf.py | 62 - .../python2_vendor/arrow/docs/index.rst | 566 ----- .../ftrack/python2_vendor/arrow/docs/make.bat | 35 - .../python2_vendor/arrow/docs/releases.rst | 3 - .../python2_vendor/arrow/requirements.txt | 14 - .../ftrack/python2_vendor/arrow/setup.cfg | 2 - .../ftrack/python2_vendor/arrow/setup.py | 50 - .../python2_vendor/arrow/tests/__init__.py | 0 .../python2_vendor/arrow/tests/conftest.py | 76 - .../python2_vendor/arrow/tests/test_api.py | 28 - .../python2_vendor/arrow/tests/test_arrow.py | 2150 ----------------- .../arrow/tests/test_factory.py | 390 --- .../arrow/tests/test_formatter.py | 282 --- .../arrow/tests/test_locales.py | 1352 ----------- .../python2_vendor/arrow/tests/test_parser.py | 1657 ------------- .../python2_vendor/arrow/tests/test_util.py | 81 - .../python2_vendor/arrow/tests/utils.py | 16 - .../ftrack/python2_vendor/arrow/tox.ini | 53 - .../python/python_2}/arrow/__init__.py | 0 .../python/python_2}/arrow/_version.py | 0 .../python/python_2}/arrow/api.py | 0 .../python/python_2}/arrow/arrow.py | 0 .../python/python_2}/arrow/constants.py | 0 .../python/python_2}/arrow/factory.py | 0 .../python/python_2}/arrow/formatter.py | 0 .../python/python_2}/arrow/locales.py | 0 .../python/python_2}/arrow/parser.py | 0 .../python/python_2}/arrow/util.py | 0 pyproject.toml | 1 + 39 files changed, 3 insertions(+), 8072 deletions(-) delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/.gitignore delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/.pre-commit-config.yaml delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/CHANGELOG.rst delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/LICENSE delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/MANIFEST.in delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/Makefile delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/README.rst delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/docs/Makefile delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/docs/conf.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/docs/index.rst delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/docs/make.bat delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/docs/releases.rst delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/requirements.txt delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/setup.cfg delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/setup.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/__init__.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/conftest.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/test_api.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/test_arrow.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/test_factory.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/test_formatter.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/test_locales.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/test_parser.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/test_util.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tests/utils.py delete mode 100644 openpype/modules/ftrack/python2_vendor/arrow/tox.ini rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/__init__.py (100%) rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/_version.py (100%) rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/api.py (100%) rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/arrow.py (100%) rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/constants.py (100%) rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/factory.py (100%) rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/formatter.py (100%) rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/locales.py (100%) rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/parser.py (100%) rename openpype/{modules/ftrack/python2_vendor/arrow => vendor/python/python_2}/arrow/util.py (100%) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index 78098dd9ba..1a6991329f 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -1,5 +1,5 @@ import os -import datetime +import arrow import collections import json @@ -566,7 +566,7 @@ def convert_v4_version_to_v3(version): output_data[dst_key] = version[src_key] if "createdAt" in version: - created_at = datetime.datetime.fromisoformat(version["createdAt"]) + created_at = arrow.get(version["createdAt"]) output_data["time"] = created_at.strftime("%Y%m%dT%H%M%SZ") output["data"] = output_data diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index d61b5f0b26..bc7216d734 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -124,8 +124,6 @@ class FtrackModule( python_paths = [ # `python-ftrack-api` os.path.join(python_2_vendor, "ftrack-python-api", "source"), - # `arrow` - os.path.join(python_2_vendor, "arrow"), # `builtins` from `python-future` # - `python-future` is strict Python 2 module that cause crashes # of Python 3 scripts executed through OpenPype diff --git a/openpype/modules/ftrack/python2_vendor/arrow/.gitignore b/openpype/modules/ftrack/python2_vendor/arrow/.gitignore deleted file mode 100644 index 0448d0cf0c..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/.gitignore +++ /dev/null @@ -1,211 +0,0 @@ -README.rst.new - -# Small entry point file for debugging tasks -test.py - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -local/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# Swap -[._]*.s[a-v][a-z] -[._]*.sw[a-p] -[._]s[a-rt-v][a-z] -[._]ss[a-gi-z] -[._]sw[a-p] - -# Session -Session.vim -Sessionx.vim - -# Temporary -.netrwhist -*~ -# Auto-generated tag files -tags -# Persistent undo -[._]*.un~ - -.idea/ -.vscode/ - -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk diff --git a/openpype/modules/ftrack/python2_vendor/arrow/.pre-commit-config.yaml b/openpype/modules/ftrack/python2_vendor/arrow/.pre-commit-config.yaml deleted file mode 100644 index 1f5128595b..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/.pre-commit-config.yaml +++ /dev/null @@ -1,41 +0,0 @@ -default_language_version: - python: python3 -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: fix-encoding-pragma - exclude: ^arrow/_version.py - - id: requirements-txt-fixer - - id: check-ast - - id: check-yaml - - id: check-case-conflict - - id: check-docstring-first - - id: check-merge-conflict - - id: debug-statements - - repo: https://github.com/timothycrosley/isort - rev: 5.4.2 - hooks: - - id: isort - - repo: https://github.com/asottile/pyupgrade - rev: v2.7.2 - hooks: - - id: pyupgrade - - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.6.0 - hooks: - - id: python-no-eval - - id: python-check-blanket-noqa - - id: rst-backticks - - repo: https://github.com/psf/black - rev: 20.8b1 - hooks: - - id: black - args: [--safe, --quiet] - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 - hooks: - - id: flake8 - additional_dependencies: [flake8-bugbear] diff --git a/openpype/modules/ftrack/python2_vendor/arrow/CHANGELOG.rst b/openpype/modules/ftrack/python2_vendor/arrow/CHANGELOG.rst deleted file mode 100644 index 0b55a4522c..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/CHANGELOG.rst +++ /dev/null @@ -1,598 +0,0 @@ -Changelog -========= - -0.17.0 (2020-10-2) -------------------- - -- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. This is the last major release to support Python 2.7 and Python 3.5. -- [NEW] Arrow now properly handles imaginary datetimes during DST shifts. For example: - -..code-block:: python - >>> just_before = arrow.get(2013, 3, 31, 1, 55, tzinfo="Europe/Paris") - >>> just_before.shift(minutes=+10) - - -..code-block:: python - >>> before = arrow.get("2018-03-10 23:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific") - >>> after = arrow.get("2018-03-11 04:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific") - >>> result=[(t, t.to("utc")) for t in arrow.Arrow.range("hour", before, after)] - >>> for r in result: - ... print(r) - ... - (, ) - (, ) - (, ) - (, ) - (, ) - -- [NEW] Added ``humanize`` week granularity translation for Tagalog. -- [CHANGE] Calls to the ``timestamp`` property now emit a ``DeprecationWarning``. In a future release, ``timestamp`` will be changed to a method to align with Python's datetime module. If you would like to continue using the property, please change your code to use the ``int_timestamp`` or ``float_timestamp`` properties instead. -- [CHANGE] Expanded and improved Catalan locale. -- [FIX] Fixed a bug that caused ``Arrow.range()`` to incorrectly cut off ranges in certain scenarios when using month, quarter, or year endings. -- [FIX] Fixed a bug that caused day of week token parsing to be case sensitive. -- [INTERNAL] A number of functions were reordered in arrow.py for better organization and grouping of related methods. This change will have no impact on usage. -- [INTERNAL] A minimum tox version is now enforced for compatibility reasons. Contributors must use tox >3.18.0 going forward. - -0.16.0 (2020-08-23) -------------------- - -- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. The 0.16.x and 0.17.x releases are the last to support Python 2.7 and 3.5. -- [NEW] Implemented `PEP 495 `_ to handle ambiguous datetimes. This is achieved by the addition of the ``fold`` attribute for Arrow objects. For example: - -.. code-block:: python - - >>> before = Arrow(2017, 10, 29, 2, 0, tzinfo='Europe/Stockholm') - - >>> before.fold - 0 - >>> before.ambiguous - True - >>> after = Arrow(2017, 10, 29, 2, 0, tzinfo='Europe/Stockholm', fold=1) - - >>> after = before.replace(fold=1) - - -- [NEW] Added ``normalize_whitespace`` flag to ``arrow.get``. This is useful for parsing log files and/or any files that may contain inconsistent spacing. For example: - -.. code-block:: python - - >>> arrow.get("Jun 1 2005 1:33PM", "MMM D YYYY H:mmA", normalize_whitespace=True) - - >>> arrow.get("2013-036 \t 04:05:06Z", normalize_whitespace=True) - - -0.15.8 (2020-07-23) -------------------- - -- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. The 0.15.x, 0.16.x, and 0.17.x releases are the last to support Python 2.7 and 3.5. -- [NEW] Added ``humanize`` week granularity translation for Czech. -- [FIX] ``arrow.get`` will now pick sane defaults when weekdays are passed with particular token combinations, see `#446 `_. -- [INTERNAL] Moved arrow to an organization. The repo can now be found `here `_. -- [INTERNAL] Started issuing deprecation warnings for Python 2.7 and 3.5. -- [INTERNAL] Added Python 3.9 to CI pipeline. - -0.15.7 (2020-06-19) -------------------- - -- [NEW] Added a number of built-in format strings. See the `docs `_ for a complete list of supported formats. For example: - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw.format(arrow.FORMAT_COOKIE) - 'Wednesday, 27-May-2020 10:30:35 UTC' - -- [NEW] Arrow is now fully compatible with Python 3.9 and PyPy3. -- [NEW] Added Makefile, tox.ini, and requirements.txt files to the distribution bundle. -- [NEW] Added French Canadian and Swahili locales. -- [NEW] Added ``humanize`` week granularity translation for Hebrew, Greek, Macedonian, Swedish, Slovak. -- [FIX] ms and μs timestamps are now normalized in ``arrow.get()``, ``arrow.fromtimestamp()``, and ``arrow.utcfromtimestamp()``. For example: - -.. code-block:: python - - >>> ts = 1591161115194556 - >>> arw = arrow.get(ts) - - >>> arw.timestamp - 1591161115 - -- [FIX] Refactored and updated Macedonian, Hebrew, Korean, and Portuguese locales. - -0.15.6 (2020-04-29) -------------------- - -- [NEW] Added support for parsing and formatting `ISO 8601 week dates `_ via a new token ``W``, for example: - -.. code-block:: python - - >>> arrow.get("2013-W29-6", "W") - - >>> utc=arrow.utcnow() - >>> utc - - >>> utc.format("W") - '2020-W04-4' - -- [NEW] Formatting with ``x`` token (microseconds) is now possible, for example: - -.. code-block:: python - - >>> dt = arrow.utcnow() - >>> dt.format("x") - '1585669870688329' - >>> dt.format("X") - '1585669870' - -- [NEW] Added ``humanize`` week granularity translation for German, Italian, Polish & Taiwanese locales. -- [FIX] Consolidated and simplified German locales. -- [INTERNAL] Moved testing suite from nosetest/Chai to pytest/pytest-mock. -- [INTERNAL] Converted xunit-style setup and teardown functions in tests to pytest fixtures. -- [INTERNAL] Setup Github Actions for CI alongside Travis. -- [INTERNAL] Help support Arrow's future development by donating to the project on `Open Collective `_. - -0.15.5 (2020-01-03) -------------------- - -- [WARN] Python 2 reached EOL on 2020-01-01. arrow will **drop support** for Python 2 in a future release to be decided (see `#739 `_). -- [NEW] Added bounds parameter to ``span_range``, ``interval`` and ``span`` methods. This allows you to include or exclude the start and end values. -- [NEW] ``arrow.get()`` can now create arrow objects from a timestamp with a timezone, for example: - -.. code-block:: python - - >>> arrow.get(1367900664, tzinfo=tz.gettz('US/Pacific')) - - -- [NEW] ``humanize`` can now combine multiple levels of granularity, for example: - -.. code-block:: python - - >>> later140 = arrow.utcnow().shift(seconds=+8400) - >>> later140.humanize(granularity="minute") - 'in 139 minutes' - >>> later140.humanize(granularity=["hour", "minute"]) - 'in 2 hours and 19 minutes' - -- [NEW] Added Hong Kong locale (``zh_hk``). -- [NEW] Added ``humanize`` week granularity translation for Dutch. -- [NEW] Numbers are now displayed when using the seconds granularity in ``humanize``. -- [CHANGE] ``range`` now supports both the singular and plural forms of the ``frames`` argument (e.g. day and days). -- [FIX] Improved parsing of strings that contain punctuation. -- [FIX] Improved behaviour of ``humanize`` when singular seconds are involved. - -0.15.4 (2019-11-02) -------------------- - -- [FIX] Fixed an issue that caused package installs to fail on Conda Forge. - -0.15.3 (2019-11-02) -------------------- - -- [NEW] ``factory.get()`` can now create arrow objects from a ISO calendar tuple, for example: - -.. code-block:: python - - >>> arrow.get((2013, 18, 7)) - - -- [NEW] Added a new token ``x`` to allow parsing of integer timestamps with milliseconds and microseconds. -- [NEW] Formatting now supports escaping of characters using the same syntax as parsing, for example: - -.. code-block:: python - - >>> arw = arrow.now() - >>> fmt = "YYYY-MM-DD h [h] m" - >>> arw.format(fmt) - '2019-11-02 3 h 32' - -- [NEW] Added ``humanize`` week granularity translations for Chinese, Spanish and Vietnamese. -- [CHANGE] Added ``ParserError`` to module exports. -- [FIX] Added support for midnight at end of day. See `#703 `_ for details. -- [INTERNAL] Created Travis build for macOS. -- [INTERNAL] Test parsing and formatting against full timezone database. - -0.15.2 (2019-09-14) -------------------- - -- [NEW] Added ``humanize`` week granularity translations for Portuguese and Brazilian Portuguese. -- [NEW] Embedded changelog within docs and added release dates to versions. -- [FIX] Fixed a bug that caused test failures on Windows only, see `#668 `_ for details. - -0.15.1 (2019-09-10) -------------------- - -- [NEW] Added ``humanize`` week granularity translations for Japanese. -- [FIX] Fixed a bug that caused Arrow to fail when passed a negative timestamp string. -- [FIX] Fixed a bug that caused Arrow to fail when passed a datetime object with ``tzinfo`` of type ``StaticTzInfo``. - -0.15.0 (2019-09-08) -------------------- - -- [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: ``arrow.get("1998-045")``, ``arrow.get("1998-45", "YYYY-DDD")``, ``arrow.get("1998-045", "YYYY-DDDD")``. -- [NEW] ISO 8601 basic format for dates and times is now supported (e.g. ``YYYYMMDDTHHmmssZ``). -- [NEW] Added ``humanize`` week granularity translations for French, Russian and Swiss German locales. -- [CHANGE] Timestamps of type ``str`` are no longer supported **without a format string** in the ``arrow.get()`` method. This change was made to support the ISO 8601 basic format and to address bugs such as `#447 `_. - -The following will NOT work in v0.15.0: - -.. code-block:: python - - >>> arrow.get("1565358758") - >>> arrow.get("1565358758.123413") - -The following will work in v0.15.0: - -.. code-block:: python - - >>> arrow.get("1565358758", "X") - >>> arrow.get("1565358758.123413", "X") - >>> arrow.get(1565358758) - >>> arrow.get(1565358758.123413) - -- [CHANGE] When a meridian token (a|A) is passed and no meridians are available for the specified locale (e.g. unsupported or untranslated) a ``ParserError`` is raised. -- [CHANGE] The timestamp token (``X``) will now match float timestamps of type ``str``: ``arrow.get(“1565358758.123415”, “X”)``. -- [CHANGE] Strings with leading and/or trailing whitespace will no longer be parsed without a format string. Please see `the docs `_ for ways to handle this. -- [FIX] The timestamp token (``X``) will now only match on strings that **strictly contain integers and floats**, preventing incorrect matches. -- [FIX] Most instances of ``arrow.get()`` returning an incorrect ``Arrow`` object from a partial parsing match have been eliminated. The following issue have been addressed: `#91 `_, `#196 `_, `#396 `_, `#434 `_, `#447 `_, `#456 `_, `#519 `_, `#538 `_, `#560 `_. - -0.14.7 (2019-09-04) -------------------- - -- [CHANGE] ``ArrowParseWarning`` will no longer be printed on every call to ``arrow.get()`` with a datetime string. The purpose of the warning was to start a conversation about the upcoming 0.15.0 changes and we appreciate all the feedback that the community has given us! - -0.14.6 (2019-08-28) -------------------- - -- [NEW] Added support for ``week`` granularity in ``Arrow.humanize()``. For example, ``arrow.utcnow().shift(weeks=-1).humanize(granularity="week")`` outputs "a week ago". This change introduced two new untranslated words, ``week`` and ``weeks``, to all locale dictionaries, so locale contributions are welcome! -- [NEW] Fully translated the Brazilian Portugese locale. -- [CHANGE] Updated the Macedonian locale to inherit from a Slavic base. -- [FIX] Fixed a bug that caused ``arrow.get()`` to ignore tzinfo arguments of type string (e.g. ``arrow.get(tzinfo="Europe/Paris")``). -- [FIX] Fixed a bug that occurred when ``arrow.Arrow()`` was instantiated with a ``pytz`` tzinfo object. -- [FIX] Fixed a bug that caused Arrow to fail when passed a sub-second token, that when rounded, had a value greater than 999999 (e.g. ``arrow.get("2015-01-12T01:13:15.9999995")``). Arrow should now accurately propagate the rounding for large sub-second tokens. - -0.14.5 (2019-08-09) -------------------- - -- [NEW] Added Afrikaans locale. -- [CHANGE] Removed deprecated ``replace`` shift functionality. Users looking to pass plural properties to the ``replace`` function to shift values should use ``shift`` instead. -- [FIX] Fixed bug that occurred when ``factory.get()`` was passed a locale kwarg. - -0.14.4 (2019-07-30) -------------------- - -- [FIX] Fixed a regression in 0.14.3 that prevented a tzinfo argument of type string to be passed to the ``get()`` function. Functionality such as ``arrow.get("2019072807", "YYYYMMDDHH", tzinfo="UTC")`` should work as normal again. -- [CHANGE] Moved ``backports.functools_lru_cache`` dependency from ``extra_requires`` to ``install_requires`` for ``Python 2.7`` installs to fix `#495 `_. - -0.14.3 (2019-07-28) -------------------- - -- [NEW] Added full support for Python 3.8. -- [CHANGE] Added warnings for upcoming factory.get() parsing changes in 0.15.0. Please see `#612 `_ for full details. -- [FIX] Extensive refactor and update of documentation. -- [FIX] factory.get() can now construct from kwargs. -- [FIX] Added meridians to Spanish Locale. - -0.14.2 (2019-06-06) -------------------- - -- [CHANGE] Travis CI builds now use tox to lint and run tests. -- [FIX] Fixed UnicodeDecodeError on certain locales (#600). - -0.14.1 (2019-06-06) -------------------- - -- [FIX] Fixed ``ImportError: No module named 'dateutil'`` (#598). - -0.14.0 (2019-06-06) -------------------- - -- [NEW] Added provisional support for Python 3.8. -- [CHANGE] Removed support for EOL Python 3.4. -- [FIX] Updated setup.py with modern Python standards. -- [FIX] Upgraded dependencies to latest versions. -- [FIX] Enabled flake8 and black on travis builds. -- [FIX] Formatted code using black and isort. - -0.13.2 (2019-05-30) -------------------- - -- [NEW] Add is_between method. -- [FIX] Improved humanize behaviour for near zero durations (#416). -- [FIX] Correct humanize behaviour with future days (#541). -- [FIX] Documentation updates. -- [FIX] Improvements to German Locale. - -0.13.1 (2019-02-17) -------------------- - -- [NEW] Add support for Python 3.7. -- [CHANGE] Remove deprecation decorators for Arrow.range(), Arrow.span_range() and Arrow.interval(), all now return generators, wrap with list() to get old behavior. -- [FIX] Documentation and docstring updates. - -0.13.0 (2019-01-09) -------------------- - -- [NEW] Added support for Python 3.6. -- [CHANGE] Drop support for Python 2.6/3.3. -- [CHANGE] Return generator instead of list for Arrow.range(), Arrow.span_range() and Arrow.interval(). -- [FIX] Make arrow.get() work with str & tzinfo combo. -- [FIX] Make sure special RegEx characters are escaped in format string. -- [NEW] Added support for ZZZ when formatting. -- [FIX] Stop using datetime.utcnow() in internals, use datetime.now(UTC) instead. -- [FIX] Return NotImplemented instead of TypeError in arrow math internals. -- [NEW] Added Estonian Locale. -- [FIX] Small fixes to Greek locale. -- [FIX] TagalogLocale improvements. -- [FIX] Added test requirements to setup. -- [FIX] Improve docs for get, now and utcnow methods. -- [FIX] Correct typo in depreciation warning. - -0.12.1 ------- - -- [FIX] Allow universal wheels to be generated and reliably installed. -- [FIX] Make humanize respect only_distance when granularity argument is also given. - -0.12.0 ------- - -- [FIX] Compatibility fix for Python 2.x - -0.11.0 ------- - -- [FIX] Fix grammar of ArabicLocale -- [NEW] Add Nepali Locale -- [FIX] Fix month name + rename AustriaLocale -> AustrianLocale -- [FIX] Fix typo in Basque Locale -- [FIX] Fix grammar in PortugueseBrazilian locale -- [FIX] Remove pip --user-mirrors flag -- [NEW] Add Indonesian Locale - -0.10.0 ------- - -- [FIX] Fix getattr off by one for quarter -- [FIX] Fix negative offset for UTC -- [FIX] Update arrow.py - -0.9.0 ------ - -- [NEW] Remove duplicate code -- [NEW] Support gnu date iso 8601 -- [NEW] Add support for universal wheels -- [NEW] Slovenian locale -- [NEW] Slovak locale -- [NEW] Romanian locale -- [FIX] respect limit even if end is defined range -- [FIX] Separate replace & shift functions -- [NEW] Added tox -- [FIX] Fix supported Python versions in documentation -- [NEW] Azerbaijani locale added, locale issue fixed in Turkish. -- [FIX] Format ParserError's raise message - -0.8.0 ------ - -- [] - -0.7.1 ------ - -- [NEW] Esperanto locale (batisteo) - -0.7.0 ------ - -- [FIX] Parse localized strings #228 (swistakm) -- [FIX] Modify tzinfo parameter in ``get`` api #221 (bottleimp) -- [FIX] Fix Czech locale (PrehistoricTeam) -- [FIX] Raise TypeError when adding/subtracting non-dates (itsmeolivia) -- [FIX] Fix pytz conversion error (Kudo) -- [FIX] Fix overzealous time truncation in span_range (kdeldycke) -- [NEW] Humanize for time duration #232 (ybrs) -- [NEW] Add Thai locale (sipp11) -- [NEW] Adding Belarusian (be) locale (oire) -- [NEW] Search date in strings (beenje) -- [NEW] Note that arrow's tokens differ from strptime's. (offby1) - -0.6.0 ------ - -- [FIX] Added support for Python 3 -- [FIX] Avoid truncating oversized epoch timestamps. Fixes #216. -- [FIX] Fixed month abbreviations for Ukrainian -- [FIX] Fix typo timezone -- [FIX] A couple of dialect fixes and two new languages -- [FIX] Spanish locale: ``Miercoles`` should have acute accent -- [Fix] Fix Finnish grammar -- [FIX] Fix typo in 'Arrow.floor' docstring -- [FIX] Use read() utility to open README -- [FIX] span_range for week frame -- [NEW] Add minimal support for fractional seconds longer than six digits. -- [NEW] Adding locale support for Marathi (mr) -- [NEW] Add count argument to span method -- [NEW] Improved docs - -0.5.1 - 0.5.4 -------------- - -- [FIX] test the behavior of simplejson instead of calling for_json directly (tonyseek) -- [FIX] Add Hebrew Locale (doodyparizada) -- [FIX] Update documentation location (andrewelkins) -- [FIX] Update setup.py Development Status level (andrewelkins) -- [FIX] Case insensitive month match (cshowe) - -0.5.0 ------ - -- [NEW] struct_time addition. (mhworth) -- [NEW] Version grep (eirnym) -- [NEW] Default to ISO 8601 format (emonty) -- [NEW] Raise TypeError on comparison (sniekamp) -- [NEW] Adding Macedonian(mk) locale (krisfremen) -- [FIX] Fix for ISO seconds and fractional seconds (sdispater) (andrewelkins) -- [FIX] Use correct Dutch wording for "hours" (wbolster) -- [FIX] Complete the list of english locales (indorilftw) -- [FIX] Change README to reStructuredText (nyuszika7h) -- [FIX] Parse lower-cased 'h' (tamentis) -- [FIX] Slight modifications to Dutch locale (nvie) - -0.4.4 ------ - -- [NEW] Include the docs in the released tarball -- [NEW] Czech localization Czech localization for Arrow -- [NEW] Add fa_ir to locales -- [FIX] Fixes parsing of time strings with a final Z -- [FIX] Fixes ISO parsing and formatting for fractional seconds -- [FIX] test_fromtimestamp sp -- [FIX] some typos fixed -- [FIX] removed an unused import statement -- [FIX] docs table fix -- [FIX] Issue with specify 'X' template and no template at all to arrow.get -- [FIX] Fix "import" typo in docs/index.rst -- [FIX] Fix unit tests for zero passed -- [FIX] Update layout.html -- [FIX] In Norwegian and new Norwegian months and weekdays should not be capitalized -- [FIX] Fixed discrepancy between specifying 'X' to arrow.get and specifying no template - -0.4.3 ------ - -- [NEW] Turkish locale (Emre) -- [NEW] Arabic locale (Mosab Ahmad) -- [NEW] Danish locale (Holmars) -- [NEW] Icelandic locale (Holmars) -- [NEW] Hindi locale (Atmb4u) -- [NEW] Malayalam locale (Atmb4u) -- [NEW] Finnish locale (Stormpat) -- [NEW] Portuguese locale (Danielcorreia) -- [NEW] ``h`` and ``hh`` strings are now supported (Averyonghub) -- [FIX] An incorrect inflection in the Polish locale has been fixed (Avalanchy) -- [FIX] ``arrow.get`` now properly handles ``Date`` (Jaapz) -- [FIX] Tests are now declared in ``setup.py`` and the manifest (Pypingou) -- [FIX] ``__version__`` has been added to ``__init__.py`` (Sametmax) -- [FIX] ISO 8601 strings can be parsed without a separator (Ivandiguisto / Root) -- [FIX] Documentation is now more clear regarding some inputs on ``arrow.get`` (Eriktaubeneck) -- [FIX] Some documentation links have been fixed (Vrutsky) -- [FIX] Error messages for parse errors are now more descriptive (Maciej Albin) -- [FIX] The parser now correctly checks for separators in strings (Mschwager) - -0.4.2 ------ - -- [NEW] Factory ``get`` method now accepts a single ``Arrow`` argument. -- [NEW] Tokens SSSS, SSSSS and SSSSSS are supported in parsing. -- [NEW] ``Arrow`` objects have a ``float_timestamp`` property. -- [NEW] Vietnamese locale (Iu1nguoi) -- [NEW] Factory ``get`` method now accepts a list of format strings (Dgilland) -- [NEW] A MANIFEST.in file has been added (Pypingou) -- [NEW] Tests can be run directly from ``setup.py`` (Pypingou) -- [FIX] Arrow docs now list 'day of week' format tokens correctly (Rudolphfroger) -- [FIX] Several issues with the Korean locale have been resolved (Yoloseem) -- [FIX] ``humanize`` now correctly returns unicode (Shvechikov) -- [FIX] ``Arrow`` objects now pickle / unpickle correctly (Yoloseem) - -0.4.1 ------ - -- [NEW] Table / explanation of formatting & parsing tokens in docs -- [NEW] Brazilian locale (Augusto2112) -- [NEW] Dutch locale (OrangeTux) -- [NEW] Italian locale (Pertux) -- [NEW] Austrain locale (LeChewbacca) -- [NEW] Tagalog locale (Marksteve) -- [FIX] Corrected spelling and day numbers in German locale (LeChewbacca) -- [FIX] Factory ``get`` method should now handle unicode strings correctly (Bwells) -- [FIX] Midnight and noon should now parse and format correctly (Bwells) - -0.4.0 ------ - -- [NEW] Format-free ISO 8601 parsing in factory ``get`` method -- [NEW] Support for 'week' / 'weeks' in ``span``, ``range``, ``span_range``, ``floor`` and ``ceil`` -- [NEW] Support for 'weeks' in ``replace`` -- [NEW] Norwegian locale (Martinp) -- [NEW] Japanese locale (CortYuming) -- [FIX] Timezones no longer show the wrong sign when formatted (Bean) -- [FIX] Microseconds are parsed correctly from strings (Bsidhom) -- [FIX] Locale day-of-week is no longer off by one (Cynddl) -- [FIX] Corrected plurals of Ukrainian and Russian nouns (Catchagain) -- [CHANGE] Old 0.1 ``arrow`` module method removed -- [CHANGE] Dropped timestamp support in ``range`` and ``span_range`` (never worked correctly) -- [CHANGE] Dropped parsing of single string as tz string in factory ``get`` method (replaced by ISO 8601) - -0.3.5 ------ - -- [NEW] French locale (Cynddl) -- [NEW] Spanish locale (Slapresta) -- [FIX] Ranges handle multiple timezones correctly (Ftobia) - -0.3.4 ------ - -- [FIX] Humanize no longer sometimes returns the wrong month delta -- [FIX] ``__format__`` works correctly with no format string - -0.3.3 ------ - -- [NEW] Python 2.6 support -- [NEW] Initial support for locale-based parsing and formatting -- [NEW] ArrowFactory class, now proxied as the module API -- [NEW] ``factory`` api method to obtain a factory for a custom type -- [FIX] Python 3 support and tests completely ironed out - -0.3.2 ------ - -- [NEW] Python 3+ support - -0.3.1 ------ - -- [FIX] The old ``arrow`` module function handles timestamps correctly as it used to - -0.3.0 ------ - -- [NEW] ``Arrow.replace`` method -- [NEW] Accept timestamps, datetimes and Arrows for datetime inputs, where reasonable -- [FIX] ``range`` and ``span_range`` respect end and limit parameters correctly -- [CHANGE] Arrow objects are no longer mutable -- [CHANGE] Plural attribute name semantics altered: single -> absolute, plural -> relative -- [CHANGE] Plural names no longer supported as properties (e.g. ``arrow.utcnow().years``) - -0.2.1 ------ - -- [NEW] Support for localized humanization -- [NEW] English, Russian, Greek, Korean, Chinese locales - -0.2.0 ------ - -- **REWRITE** -- [NEW] Date parsing -- [NEW] Date formatting -- [NEW] ``floor``, ``ceil`` and ``span`` methods -- [NEW] ``datetime`` interface implementation -- [NEW] ``clone`` method -- [NEW] ``get``, ``now`` and ``utcnow`` API methods - -0.1.6 ------ - -- [NEW] Humanized time deltas -- [NEW] ``__eq__`` implemented -- [FIX] Issues with conversions related to daylight savings time resolved -- [CHANGE] ``__str__`` uses ISO formatting - -0.1.5 ------ - -- **Started tracking changes** -- [NEW] Parsing of ISO-formatted time zone offsets (e.g. '+02:30', '-05:00') -- [NEW] Resolved some issues with timestamps and delta / Olson time zones diff --git a/openpype/modules/ftrack/python2_vendor/arrow/LICENSE b/openpype/modules/ftrack/python2_vendor/arrow/LICENSE deleted file mode 100644 index 2bef500de7..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2019 Chris Smith - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/openpype/modules/ftrack/python2_vendor/arrow/MANIFEST.in b/openpype/modules/ftrack/python2_vendor/arrow/MANIFEST.in deleted file mode 100644 index d9955ed96a..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include LICENSE CHANGELOG.rst README.rst Makefile requirements.txt tox.ini -recursive-include tests *.py -recursive-include docs *.py *.rst *.bat Makefile diff --git a/openpype/modules/ftrack/python2_vendor/arrow/Makefile b/openpype/modules/ftrack/python2_vendor/arrow/Makefile deleted file mode 100644 index f294985dc6..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/Makefile +++ /dev/null @@ -1,44 +0,0 @@ -.PHONY: auto test docs clean - -auto: build38 - -build27: PYTHON_VER = python2.7 -build35: PYTHON_VER = python3.5 -build36: PYTHON_VER = python3.6 -build37: PYTHON_VER = python3.7 -build38: PYTHON_VER = python3.8 -build39: PYTHON_VER = python3.9 - -build27 build35 build36 build37 build38 build39: clean - virtualenv venv --python=$(PYTHON_VER) - . venv/bin/activate; \ - pip install -r requirements.txt; \ - pre-commit install - -test: - rm -f .coverage coverage.xml - . venv/bin/activate; pytest - -lint: - . venv/bin/activate; pre-commit run --all-files --show-diff-on-failure - -docs: - rm -rf docs/_build - . venv/bin/activate; cd docs; make html - -clean: clean-dist - rm -rf venv .pytest_cache ./**/__pycache__ - rm -f .coverage coverage.xml ./**/*.pyc - -clean-dist: - rm -rf dist build .egg .eggs arrow.egg-info - -build-dist: - . venv/bin/activate; \ - pip install -U setuptools twine wheel; \ - python setup.py sdist bdist_wheel - -upload-dist: - . venv/bin/activate; twine upload dist/* - -publish: test clean-dist build-dist upload-dist clean-dist diff --git a/openpype/modules/ftrack/python2_vendor/arrow/README.rst b/openpype/modules/ftrack/python2_vendor/arrow/README.rst deleted file mode 100644 index 69f6c50d81..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/README.rst +++ /dev/null @@ -1,133 +0,0 @@ -Arrow: Better dates & times for Python -====================================== - -.. start-inclusion-marker-do-not-remove - -.. image:: https://github.com/arrow-py/arrow/workflows/tests/badge.svg?branch=master - :alt: Build Status - :target: https://github.com/arrow-py/arrow/actions?query=workflow%3Atests+branch%3Amaster - -.. image:: https://codecov.io/gh/arrow-py/arrow/branch/master/graph/badge.svg - :alt: Coverage - :target: https://codecov.io/gh/arrow-py/arrow - -.. image:: https://img.shields.io/pypi/v/arrow.svg - :alt: PyPI Version - :target: https://pypi.python.org/pypi/arrow - -.. image:: https://img.shields.io/pypi/pyversions/arrow.svg - :alt: Supported Python Versions - :target: https://pypi.python.org/pypi/arrow - -.. image:: https://img.shields.io/pypi/l/arrow.svg - :alt: License - :target: https://pypi.python.org/pypi/arrow - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :alt: Code Style: Black - :target: https://github.com/psf/black - - -**Arrow** is a Python library that offers a sensible and human-friendly approach to creating, manipulating, formatting and converting dates, times and timestamps. It implements and updates the datetime type, plugging gaps in functionality and providing an intelligent module API that supports many common creation scenarios. Simply put, it helps you work with dates and times with fewer imports and a lot less code. - -Arrow is named after the `arrow of time `_ and is heavily inspired by `moment.js `_ and `requests `_. - -Why use Arrow over built-in modules? ------------------------------------- - -Python's standard library and some other low-level modules have near-complete date, time and timezone functionality, but don't work very well from a usability perspective: - -- Too many modules: datetime, time, calendar, dateutil, pytz and more -- Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc. -- Timezones and timestamp conversions are verbose and unpleasant -- Timezone naivety is the norm -- Gaps in functionality: ISO 8601 parsing, timespans, humanization - -Features --------- - -- Fully-implemented, drop-in replacement for datetime -- Supports Python 2.7, 3.5, 3.6, 3.7, 3.8 and 3.9 -- Timezone-aware and UTC by default -- Provides super-simple creation options for many common input scenarios -- :code:`shift` method with support for relative offsets, including weeks -- Formats and parses strings automatically -- Wide support for ISO 8601 -- Timezone conversion -- Timestamp available as a property -- Generates time spans, ranges, floors and ceilings for time frames ranging from microsecond to year -- Humanizes and supports a growing list of contributed locales -- Extensible for your own Arrow-derived types - -Quick Start ------------ - -Installation -~~~~~~~~~~~~ - -To install Arrow, use `pip `_ or `pipenv `_: - -.. code-block:: console - - $ pip install -U arrow - -Example Usage -~~~~~~~~~~~~~ - -.. code-block:: python - - >>> import arrow - >>> arrow.get('2013-05-11T21:23:58.970460+07:00') - - - >>> utc = arrow.utcnow() - >>> utc - - - >>> utc = utc.shift(hours=-1) - >>> utc - - - >>> local = utc.to('US/Pacific') - >>> local - - - >>> local.timestamp - 1368303838 - - >>> local.format() - '2013-05-11 13:23:58 -07:00' - - >>> local.format('YYYY-MM-DD HH:mm:ss ZZ') - '2013-05-11 13:23:58 -07:00' - - >>> local.humanize() - 'an hour ago' - - >>> local.humanize(locale='ko_kr') - '1시간 전' - -.. end-inclusion-marker-do-not-remove - -Documentation -------------- - -For full documentation, please visit `arrow.readthedocs.io `_. - -Contributing ------------- - -Contributions are welcome for both code and localizations (adding and updating locales). Begin by gaining familiarity with the Arrow library and its features. Then, jump into contributing: - -#. Find an issue or feature to tackle on the `issue tracker `_. Issues marked with the `"good first issue" label `_ may be a great place to start! -#. Fork `this repository `_ on GitHub and begin making changes in a branch. -#. Add a few tests to ensure that the bug was fixed or the feature works as expected. -#. Run the entire test suite and linting checks by running one of the following commands: :code:`tox` (if you have `tox `_ installed) **OR** :code:`make build38 && make test && make lint` (if you do not have Python 3.8 installed, replace :code:`build38` with the latest Python version on your system). -#. Submit a pull request and await feedback 😃. - -If you have any questions along the way, feel free to ask them `here `_. - -Support Arrow -------------- - -`Open Collective `_ is an online funding platform that provides tools to raise money and share your finances with full transparency. It is the platform of choice for individuals and companies to make one-time or recurring donations directly to the project. If you are interested in making a financial contribution, please visit the `Arrow collective `_. diff --git a/openpype/modules/ftrack/python2_vendor/arrow/docs/Makefile b/openpype/modules/ftrack/python2_vendor/arrow/docs/Makefile deleted file mode 100644 index d4bb2cbb9e..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/openpype/modules/ftrack/python2_vendor/arrow/docs/conf.py b/openpype/modules/ftrack/python2_vendor/arrow/docs/conf.py deleted file mode 100644 index aaf3c50822..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/docs/conf.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- - -# -- Path setup -------------------------------------------------------------- - -import io -import os -import sys - -sys.path.insert(0, os.path.abspath("..")) - -about = {} -with io.open("../arrow/_version.py", "r", encoding="utf-8") as f: - exec(f.read(), about) - -# -- Project information ----------------------------------------------------- - -project = u"Arrow 🏹" -copyright = "2020, Chris Smith" -author = "Chris Smith" - -release = about["__version__"] - -# -- General configuration --------------------------------------------------- - -extensions = ["sphinx.ext.autodoc"] - -templates_path = [] - -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - -master_doc = "index" -source_suffix = ".rst" -pygments_style = "sphinx" - -language = None - -# -- Options for HTML output ------------------------------------------------- - -html_theme = "alabaster" -html_theme_path = [] -html_static_path = [] - -html_show_sourcelink = False -html_show_sphinx = False -html_show_copyright = True - -# https://alabaster.readthedocs.io/en/latest/customization.html -html_theme_options = { - "description": "Arrow is a sensible and human-friendly approach to dates, times and timestamps.", - "github_user": "arrow-py", - "github_repo": "arrow", - "github_banner": True, - "show_related": False, - "show_powered_by": False, - "github_button": True, - "github_type": "star", - "github_count": "true", # must be a string -} - -html_sidebars = { - "**": ["about.html", "localtoc.html", "relations.html", "searchbox.html"] -} diff --git a/openpype/modules/ftrack/python2_vendor/arrow/docs/index.rst b/openpype/modules/ftrack/python2_vendor/arrow/docs/index.rst deleted file mode 100644 index e2830b04f3..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/docs/index.rst +++ /dev/null @@ -1,566 +0,0 @@ -Arrow: Better dates & times for Python -====================================== - -Release v\ |release| (`Installation`_) (`Changelog `_) - -.. include:: ../README.rst - :start-after: start-inclusion-marker-do-not-remove - :end-before: end-inclusion-marker-do-not-remove - -User's Guide ------------- - -Creation -~~~~~~~~ - -Get 'now' easily: - -.. code-block:: python - - >>> arrow.utcnow() - - - >>> arrow.now() - - - >>> arrow.now('US/Pacific') - - -Create from timestamps (:code:`int` or :code:`float`): - -.. code-block:: python - - >>> arrow.get(1367900664) - - - >>> arrow.get(1367900664.152325) - - -Use a naive or timezone-aware datetime, or flexibly specify a timezone: - -.. code-block:: python - - >>> arrow.get(datetime.utcnow()) - - - >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') - - - >>> from dateutil import tz - >>> arrow.get(datetime(2013, 5, 5), tz.gettz('US/Pacific')) - - - >>> arrow.get(datetime.now(tz.gettz('US/Pacific'))) - - -Parse from a string: - -.. code-block:: python - - >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') - - -Search a date in a string: - -.. code-block:: python - - >>> arrow.get('June was born in May 1980', 'MMMM YYYY') - - -Some ISO 8601 compliant strings are recognized and parsed without a format string: - - >>> arrow.get('2013-09-30T15:34:00.000-07:00') - - -Arrow objects can be instantiated directly too, with the same arguments as a datetime: - -.. code-block:: python - - >>> arrow.get(2013, 5, 5) - - - >>> arrow.Arrow(2013, 5, 5) - - -Properties -~~~~~~~~~~ - -Get a datetime or timestamp representation: - -.. code-block:: python - - >>> a = arrow.utcnow() - >>> a.datetime - datetime.datetime(2013, 5, 7, 4, 38, 15, 447644, tzinfo=tzutc()) - - >>> a.timestamp - 1367901495 - -Get a naive datetime, and tzinfo: - -.. code-block:: python - - >>> a.naive - datetime.datetime(2013, 5, 7, 4, 38, 15, 447644) - - >>> a.tzinfo - tzutc() - -Get any datetime value: - -.. code-block:: python - - >>> a.year - 2013 - -Call datetime functions that return properties: - -.. code-block:: python - - >>> a.date() - datetime.date(2013, 5, 7) - - >>> a.time() - datetime.time(4, 38, 15, 447644) - -Replace & Shift -~~~~~~~~~~~~~~~ - -Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime: - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw - - - >>> arw.replace(hour=4, minute=40) - - -Or, get one with attributes shifted forward or backward: - -.. code-block:: python - - >>> arw.shift(weeks=+3) - - -Even replace the timezone without altering other attributes: - -.. code-block:: python - - >>> arw.replace(tzinfo='US/Pacific') - - -Move between the earlier and later moments of an ambiguous time: - -.. code-block:: python - - >>> paris_transition = arrow.Arrow(2019, 10, 27, 2, tzinfo="Europe/Paris", fold=0) - >>> paris_transition - - >>> paris_transition.ambiguous - True - >>> paris_transition.replace(fold=1) - - -Format -~~~~~~ - -.. code-block:: python - - >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') - '2013-05-07 05:23:16 -00:00' - -Convert -~~~~~~~ - -Convert from UTC to other timezones by name or tzinfo: - -.. code-block:: python - - >>> utc = arrow.utcnow() - >>> utc - - - >>> utc.to('US/Pacific') - - - >>> utc.to(tz.gettz('US/Pacific')) - - -Or using shorthand: - -.. code-block:: python - - >>> utc.to('local') - - - >>> utc.to('local').to('utc') - - - -Humanize -~~~~~~~~ - -Humanize relative to now: - -.. code-block:: python - - >>> past = arrow.utcnow().shift(hours=-1) - >>> past.humanize() - 'an hour ago' - -Or another Arrow, or datetime: - -.. code-block:: python - - >>> present = arrow.utcnow() - >>> future = present.shift(hours=2) - >>> future.humanize(present) - 'in 2 hours' - -Indicate time as relative or include only the distance - -.. code-block:: python - - >>> present = arrow.utcnow() - >>> future = present.shift(hours=2) - >>> future.humanize(present) - 'in 2 hours' - >>> future.humanize(present, only_distance=True) - '2 hours' - - -Indicate a specific time granularity (or multiple): - -.. code-block:: python - - >>> present = arrow.utcnow() - >>> future = present.shift(minutes=66) - >>> future.humanize(present, granularity="minute") - 'in 66 minutes' - >>> future.humanize(present, granularity=["hour", "minute"]) - 'in an hour and 6 minutes' - >>> present.humanize(future, granularity=["hour", "minute"]) - 'an hour and 6 minutes ago' - >>> future.humanize(present, only_distance=True, granularity=["hour", "minute"]) - 'an hour and 6 minutes' - -Support for a growing number of locales (see ``locales.py`` for supported languages): - -.. code-block:: python - - - >>> future = arrow.utcnow().shift(hours=1) - >>> future.humanize(a, locale='ru') - 'через 2 час(а,ов)' - - -Ranges & Spans -~~~~~~~~~~~~~~ - -Get the time span of any unit: - -.. code-block:: python - - >>> arrow.utcnow().span('hour') - (, ) - -Or just get the floor and ceiling: - -.. code-block:: python - - >>> arrow.utcnow().floor('hour') - - - >>> arrow.utcnow().ceil('hour') - - -You can also get a range of time spans: - -.. code-block:: python - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.span_range('hour', start, end): - ... print r - ... - (, ) - (, ) - (, ) - (, ) - (, ) - -Or just iterate over a range of time: - -.. code-block:: python - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.range('hour', start, end): - ... print repr(r) - ... - - - - - - -.. toctree:: - :maxdepth: 2 - -Factories -~~~~~~~~~ - -Use factories to harness Arrow's module API for a custom Arrow-derived type. First, derive your type: - -.. code-block:: python - - >>> class CustomArrow(arrow.Arrow): - ... - ... def days_till_xmas(self): - ... - ... xmas = arrow.Arrow(self.year, 12, 25) - ... - ... if self > xmas: - ... xmas = xmas.shift(years=1) - ... - ... return (xmas - self).days - - -Then get and use a factory for it: - -.. code-block:: python - - >>> factory = arrow.ArrowFactory(CustomArrow) - >>> custom = factory.utcnow() - >>> custom - >>> - - >>> custom.days_till_xmas() - >>> 211 - -Supported Tokens -~~~~~~~~~~~~~~~~ - -Use the following tokens for parsing and formatting. Note that they are **not** the same as the tokens for `strptime `_: - -+--------------------------------+--------------+-------------------------------------------+ -| |Token |Output | -+================================+==============+===========================================+ -|**Year** |YYYY |2000, 2001, 2002 ... 2012, 2013 | -+--------------------------------+--------------+-------------------------------------------+ -| |YY |00, 01, 02 ... 12, 13 | -+--------------------------------+--------------+-------------------------------------------+ -|**Month** |MMMM |January, February, March ... [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |MMM |Jan, Feb, Mar ... [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |MM |01, 02, 03 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -| |M |1, 2, 3 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -|**Day of Year** |DDDD |001, 002, 003 ... 364, 365 | -+--------------------------------+--------------+-------------------------------------------+ -| |DDD |1, 2, 3 ... 364, 365 | -+--------------------------------+--------------+-------------------------------------------+ -|**Day of Month** |DD |01, 02, 03 ... 30, 31 | -+--------------------------------+--------------+-------------------------------------------+ -| |D |1, 2, 3 ... 30, 31 | -+--------------------------------+--------------+-------------------------------------------+ -| |Do |1st, 2nd, 3rd ... 30th, 31st | -+--------------------------------+--------------+-------------------------------------------+ -|**Day of Week** |dddd |Monday, Tuesday, Wednesday ... [#t2]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |ddd |Mon, Tue, Wed ... [#t2]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |d |1, 2, 3 ... 6, 7 | -+--------------------------------+--------------+-------------------------------------------+ -|**ISO week date** |W |2011-W05-4, 2019-W17 | -+--------------------------------+--------------+-------------------------------------------+ -|**Hour** |HH |00, 01, 02 ... 23, 24 | -+--------------------------------+--------------+-------------------------------------------+ -| |H |0, 1, 2 ... 23, 24 | -+--------------------------------+--------------+-------------------------------------------+ -| |hh |01, 02, 03 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -| |h |1, 2, 3 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -|**AM / PM** |A |AM, PM, am, pm [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |a |am, pm [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -|**Minute** |mm |00, 01, 02 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -| |m |0, 1, 2 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -|**Second** |ss |00, 01, 02 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -| |s |0, 1, 2 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -|**Sub-second** |S... |0, 02, 003, 000006, 123123123123... [#t3]_ | -+--------------------------------+--------------+-------------------------------------------+ -|**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |ZZ |-07:00, -06:00 ... +06:00, +07:00, +08, Z | -+--------------------------------+--------------+-------------------------------------------+ -| |Z |-0700, -0600 ... +0600, +0700, +08, Z | -+--------------------------------+--------------+-------------------------------------------+ -|**Seconds Timestamp** |X |1381685817, 1381685817.915482 ... [#t5]_ | -+--------------------------------+--------------+-------------------------------------------+ -|**ms or µs Timestamp** |x |1569980330813, 1569980330813221 | -+--------------------------------+--------------+-------------------------------------------+ - -.. rubric:: Footnotes - -.. [#t1] localization support for parsing and formatting -.. [#t2] localization support only for formatting -.. [#t3] the result is truncated to microseconds, with `half-to-even rounding `_. -.. [#t4] timezone names from `tz database `_ provided via dateutil package, note that abbreviations such as MST, PDT, BRST are unlikely to parse due to ambiguity. Use the full IANA zone name instead (Asia/Shanghai, Europe/London, America/Chicago etc). -.. [#t5] this token cannot be used for parsing timestamps out of natural language strings due to compatibility reasons - -Built-in Formats -++++++++++++++++ - -There are several formatting standards that are provided as built-in tokens. - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw.format(arrow.FORMAT_ATOM) - '2020-05-27 10:30:35+00:00' - >>> arw.format(arrow.FORMAT_COOKIE) - 'Wednesday, 27-May-2020 10:30:35 UTC' - >>> arw.format(arrow.FORMAT_RSS) - 'Wed, 27 May 2020 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC822) - 'Wed, 27 May 20 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC850) - 'Wednesday, 27-May-20 10:30:35 UTC' - >>> arw.format(arrow.FORMAT_RFC1036) - 'Wed, 27 May 20 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC1123) - 'Wed, 27 May 2020 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC2822) - 'Wed, 27 May 2020 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC3339) - '2020-05-27 10:30:35+00:00' - >>> arw.format(arrow.FORMAT_W3C) - '2020-05-27 10:30:35+00:00' - -Escaping Formats -~~~~~~~~~~~~~~~~ - -Tokens, phrases, and regular expressions in a format string can be escaped when parsing and formatting by enclosing them within square brackets. - -Tokens & Phrases -++++++++++++++++ - -Any `token `_ or phrase can be escaped as follows: - -.. code-block:: python - - >>> fmt = "YYYY-MM-DD h [h] m" - >>> arw = arrow.get("2018-03-09 8 h 40", fmt) - - >>> arw.format(fmt) - '2018-03-09 8 h 40' - - >>> fmt = "YYYY-MM-DD h [hello] m" - >>> arw = arrow.get("2018-03-09 8 hello 40", fmt) - - >>> arw.format(fmt) - '2018-03-09 8 hello 40' - - >>> fmt = "YYYY-MM-DD h [hello world] m" - >>> arw = arrow.get("2018-03-09 8 hello world 40", fmt) - - >>> arw.format(fmt) - '2018-03-09 8 hello world 40' - -This can be useful for parsing dates in different locales such as French, in which it is common to format time strings as "8 h 40" rather than "8:40". - -Regular Expressions -+++++++++++++++++++ - -You can also escape regular expressions by enclosing them within square brackets. In the following example, we are using the regular expression :code:`\s+` to match any number of whitespace characters that separate the tokens. This is useful if you do not know the number of spaces between tokens ahead of time (e.g. in log files). - -.. code-block:: python - - >>> fmt = r"ddd[\s+]MMM[\s+]DD[\s+]HH:mm:ss[\s+]YYYY" - >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) - - - >>> arrow.get("Mon \tSep 08 16:41:45 2014", fmt) - - - >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) - - -Punctuation -~~~~~~~~~~~ - -Date and time formats may be fenced on either side by one punctuation character from the following list: ``, . ; : ? ! " \` ' [ ] { } ( ) < >`` - -.. code-block:: python - - >>> arrow.get("Cool date: 2019-10-31T09:12:45.123456+04:30.", "YYYY-MM-DDTHH:mm:ss.SZZ") - - - >>> arrow.get("Tomorrow (2019-10-31) is Halloween!", "YYYY-MM-DD") - - - >>> arrow.get("Halloween is on 2019.10.31.", "YYYY.MM.DD") - - - >>> arrow.get("It's Halloween tomorrow (2019-10-31)!", "YYYY-MM-DD") - # Raises exception because there are multiple punctuation marks following the date - -Redundant Whitespace -~~~~~~~~~~~~~~~~~~~~ - -Redundant whitespace characters (spaces, tabs, and newlines) can be normalized automatically by passing in the ``normalize_whitespace`` flag to ``arrow.get``: - -.. code-block:: python - - >>> arrow.get('\t \n 2013-05-05T12:30:45.123456 \t \n', normalize_whitespace=True) - - - >>> arrow.get('2013-05-05 T \n 12:30:45\t123456', 'YYYY-MM-DD T HH:mm:ss S', normalize_whitespace=True) - - -API Guide ---------- - -arrow.arrow -~~~~~~~~~~~ - -.. automodule:: arrow.arrow - :members: - -arrow.factory -~~~~~~~~~~~~~ - -.. automodule:: arrow.factory - :members: - -arrow.api -~~~~~~~~~ - -.. automodule:: arrow.api - :members: - -arrow.locale -~~~~~~~~~~~~ - -.. automodule:: arrow.locales - :members: - :undoc-members: - -Release History ---------------- - -.. toctree:: - :maxdepth: 2 - - releases diff --git a/openpype/modules/ftrack/python2_vendor/arrow/docs/make.bat b/openpype/modules/ftrack/python2_vendor/arrow/docs/make.bat deleted file mode 100644 index 922152e96a..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/openpype/modules/ftrack/python2_vendor/arrow/docs/releases.rst b/openpype/modules/ftrack/python2_vendor/arrow/docs/releases.rst deleted file mode 100644 index 22e1e59c8c..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/docs/releases.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. _releases: - -.. include:: ../CHANGELOG.rst diff --git a/openpype/modules/ftrack/python2_vendor/arrow/requirements.txt b/openpype/modules/ftrack/python2_vendor/arrow/requirements.txt deleted file mode 100644 index df565d8384..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -backports.functools_lru_cache==1.6.1; python_version == "2.7" -dateparser==0.7.* -pre-commit==1.21.*; python_version <= "3.5" -pre-commit==2.6.*; python_version >= "3.6" -pytest==4.6.*; python_version == "2.7" -pytest==6.0.*; python_version >= "3.5" -pytest-cov==2.10.* -pytest-mock==2.0.*; python_version == "2.7" -pytest-mock==3.2.*; python_version >= "3.5" -python-dateutil==2.8.* -pytz==2019.* -simplejson==3.17.* -sphinx==1.8.*; python_version == "2.7" -sphinx==3.2.*; python_version >= "3.5" diff --git a/openpype/modules/ftrack/python2_vendor/arrow/setup.cfg b/openpype/modules/ftrack/python2_vendor/arrow/setup.cfg deleted file mode 100644 index 2a9acf13da..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/openpype/modules/ftrack/python2_vendor/arrow/setup.py b/openpype/modules/ftrack/python2_vendor/arrow/setup.py deleted file mode 100644 index dc4f0e77d5..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/setup.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -import io - -from setuptools import setup - -with io.open("README.rst", "r", encoding="utf-8") as f: - readme = f.read() - -about = {} -with io.open("arrow/_version.py", "r", encoding="utf-8") as f: - exec(f.read(), about) - -setup( - name="arrow", - version=about["__version__"], - description="Better dates & times for Python", - long_description=readme, - long_description_content_type="text/x-rst", - url="https://arrow.readthedocs.io", - author="Chris Smith", - author_email="crsmithdev@gmail.com", - license="Apache 2.0", - packages=["arrow"], - zip_safe=False, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", - install_requires=[ - "python-dateutil>=2.7.0", - "backports.functools_lru_cache>=1.2.1;python_version=='2.7'", - ], - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - ], - keywords="arrow date time datetime timestamp timezone humanize", - project_urls={ - "Repository": "https://github.com/arrow-py/arrow", - "Bug Reports": "https://github.com/arrow-py/arrow/issues", - "Documentation": "https://arrow.readthedocs.io", - }, -) diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/__init__.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/conftest.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/conftest.py deleted file mode 100644 index 5bc8a4af2e..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tests/conftest.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -from datetime import datetime - -import pytest -from dateutil import tz as dateutil_tz - -from arrow import arrow, factory, formatter, locales, parser - - -@pytest.fixture(scope="class") -def time_utcnow(request): - request.cls.arrow = arrow.Arrow.utcnow() - - -@pytest.fixture(scope="class") -def time_2013_01_01(request): - request.cls.now = arrow.Arrow.utcnow() - request.cls.arrow = arrow.Arrow(2013, 1, 1) - request.cls.datetime = datetime(2013, 1, 1) - - -@pytest.fixture(scope="class") -def time_2013_02_03(request): - request.cls.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) - - -@pytest.fixture(scope="class") -def time_2013_02_15(request): - request.cls.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) - request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) - - -@pytest.fixture(scope="class") -def time_1975_12_25(request): - request.cls.datetime = datetime( - 1975, 12, 25, 14, 15, 16, tzinfo=dateutil_tz.gettz("America/New_York") - ) - request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) - - -@pytest.fixture(scope="class") -def arrow_formatter(request): - request.cls.formatter = formatter.DateTimeFormatter() - - -@pytest.fixture(scope="class") -def arrow_factory(request): - request.cls.factory = factory.ArrowFactory() - - -@pytest.fixture(scope="class") -def lang_locales(request): - request.cls.locales = locales._locales - - -@pytest.fixture(scope="class") -def lang_locale(request): - # As locale test classes are prefixed with Test, we are dynamically getting the locale by the test class name. - # TestEnglishLocale -> EnglishLocale - name = request.cls.__name__[4:] - request.cls.locale = locales.get_locale_by_class_name(name) - - -@pytest.fixture(scope="class") -def dt_parser(request): - request.cls.parser = parser.DateTimeParser() - - -@pytest.fixture(scope="class") -def dt_parser_regex(request): - request.cls.format_regex = parser.DateTimeParser._FORMAT_RE - - -@pytest.fixture(scope="class") -def tzinfo_parser(request): - request.cls.parser = parser.TzinfoParser() diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_api.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/test_api.py deleted file mode 100644 index 9b19a27cd9..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_api.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -import arrow - - -class TestModule: - def test_get(self, mocker): - mocker.patch("arrow.api._factory.get", return_value="result") - - assert arrow.api.get() == "result" - - def test_utcnow(self, mocker): - mocker.patch("arrow.api._factory.utcnow", return_value="utcnow") - - assert arrow.api.utcnow() == "utcnow" - - def test_now(self, mocker): - mocker.patch("arrow.api._factory.now", tz="tz", return_value="now") - - assert arrow.api.now("tz") == "now" - - def test_factory(self): - class MockCustomArrowClass(arrow.Arrow): - pass - - result = arrow.api.factory(MockCustomArrowClass) - - assert isinstance(result, arrow.factory.ArrowFactory) - assert isinstance(result.utcnow(), MockCustomArrowClass) diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_arrow.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/test_arrow.py deleted file mode 100644 index b0bd20a5e3..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_arrow.py +++ /dev/null @@ -1,2150 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import calendar -import pickle -import sys -import time -from datetime import date, datetime, timedelta - -import dateutil -import pytest -import pytz -import simplejson as json -from dateutil import tz -from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE - -from arrow import arrow - -from .utils import assert_datetime_equality - - -class TestTestArrowInit: - def test_init_bad_input(self): - - with pytest.raises(TypeError): - arrow.Arrow(2013) - - with pytest.raises(TypeError): - arrow.Arrow(2013, 2) - - with pytest.raises(ValueError): - arrow.Arrow(2013, 2, 2, 12, 30, 45, 9999999) - - def test_init(self): - - result = arrow.Arrow(2013, 2, 2) - self.expected = datetime(2013, 2, 2, tzinfo=tz.tzutc()) - assert result._datetime == self.expected - - result = arrow.Arrow(2013, 2, 2, 12) - self.expected = datetime(2013, 2, 2, 12, tzinfo=tz.tzutc()) - assert result._datetime == self.expected - - result = arrow.Arrow(2013, 2, 2, 12, 30) - self.expected = datetime(2013, 2, 2, 12, 30, tzinfo=tz.tzutc()) - assert result._datetime == self.expected - - result = arrow.Arrow(2013, 2, 2, 12, 30, 45) - self.expected = datetime(2013, 2, 2, 12, 30, 45, tzinfo=tz.tzutc()) - assert result._datetime == self.expected - - result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999) - self.expected = datetime(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc()) - assert result._datetime == self.expected - - result = arrow.Arrow( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") - ) - self.expected = datetime( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") - ) - assert result._datetime == self.expected - - # regression tests for issue #626 - def test_init_pytz_timezone(self): - - result = arrow.Arrow( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=pytz.timezone("Europe/Paris") - ) - self.expected = datetime( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") - ) - assert result._datetime == self.expected - assert_datetime_equality(result._datetime, self.expected, 1) - - def test_init_with_fold(self): - before = arrow.Arrow(2017, 10, 29, 2, 0, tzinfo="Europe/Stockholm") - after = arrow.Arrow(2017, 10, 29, 2, 0, tzinfo="Europe/Stockholm", fold=1) - - assert hasattr(before, "fold") - assert hasattr(after, "fold") - - # PEP-495 requires the comparisons below to be true - assert before == after - assert before.utcoffset() != after.utcoffset() - - -class TestTestArrowFactory: - def test_now(self): - - result = arrow.Arrow.now() - - assert_datetime_equality( - result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) - ) - - def test_utcnow(self): - - result = arrow.Arrow.utcnow() - - assert_datetime_equality( - result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()) - ) - - assert result.fold == 0 - - def test_fromtimestamp(self): - - timestamp = time.time() - - result = arrow.Arrow.fromtimestamp(timestamp) - assert_datetime_equality( - result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) - ) - - result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris")) - assert_datetime_equality( - result._datetime, - datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), - ) - - result = arrow.Arrow.fromtimestamp(timestamp, tzinfo="Europe/Paris") - assert_datetime_equality( - result._datetime, - datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), - ) - - with pytest.raises(ValueError): - arrow.Arrow.fromtimestamp("invalid timestamp") - - def test_utcfromtimestamp(self): - - timestamp = time.time() - - result = arrow.Arrow.utcfromtimestamp(timestamp) - assert_datetime_equality( - result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()) - ) - - with pytest.raises(ValueError): - arrow.Arrow.utcfromtimestamp("invalid timestamp") - - def test_fromdatetime(self): - - dt = datetime(2013, 2, 3, 12, 30, 45, 1) - - result = arrow.Arrow.fromdatetime(dt) - - assert result._datetime == dt.replace(tzinfo=tz.tzutc()) - - def test_fromdatetime_dt_tzinfo(self): - - dt = datetime(2013, 2, 3, 12, 30, 45, 1, tzinfo=tz.gettz("US/Pacific")) - - result = arrow.Arrow.fromdatetime(dt) - - assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) - - def test_fromdatetime_tzinfo_arg(self): - - dt = datetime(2013, 2, 3, 12, 30, 45, 1) - - result = arrow.Arrow.fromdatetime(dt, tz.gettz("US/Pacific")) - - assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) - - def test_fromdate(self): - - dt = date(2013, 2, 3) - - result = arrow.Arrow.fromdate(dt, tz.gettz("US/Pacific")) - - assert result._datetime == datetime(2013, 2, 3, tzinfo=tz.gettz("US/Pacific")) - - def test_strptime(self): - - formatted = datetime(2013, 2, 3, 12, 30, 45).strftime("%Y-%m-%d %H:%M:%S") - - result = arrow.Arrow.strptime(formatted, "%Y-%m-%d %H:%M:%S") - assert result._datetime == datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.tzutc()) - - result = arrow.Arrow.strptime( - formatted, "%Y-%m-%d %H:%M:%S", tzinfo=tz.gettz("Europe/Paris") - ) - assert result._datetime == datetime( - 2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris") - ) - - -@pytest.mark.usefixtures("time_2013_02_03") -class TestTestArrowRepresentation: - def test_repr(self): - - result = self.arrow.__repr__() - - assert result == "".format(self.arrow._datetime.isoformat()) - - def test_str(self): - - result = self.arrow.__str__() - - assert result == self.arrow._datetime.isoformat() - - def test_hash(self): - - result = self.arrow.__hash__() - - assert result == self.arrow._datetime.__hash__() - - def test_format(self): - - result = "{:YYYY-MM-DD}".format(self.arrow) - - assert result == "2013-02-03" - - def test_bare_format(self): - - result = self.arrow.format() - - assert result == "2013-02-03 12:30:45+00:00" - - def test_format_no_format_string(self): - - result = "{}".format(self.arrow) - - assert result == str(self.arrow) - - def test_clone(self): - - result = self.arrow.clone() - - assert result is not self.arrow - assert result._datetime == self.arrow._datetime - - -@pytest.mark.usefixtures("time_2013_01_01") -class TestArrowAttribute: - def test_getattr_base(self): - - with pytest.raises(AttributeError): - self.arrow.prop - - def test_getattr_week(self): - - assert self.arrow.week == 1 - - def test_getattr_quarter(self): - # start dates - q1 = arrow.Arrow(2013, 1, 1) - q2 = arrow.Arrow(2013, 4, 1) - q3 = arrow.Arrow(2013, 8, 1) - q4 = arrow.Arrow(2013, 10, 1) - assert q1.quarter == 1 - assert q2.quarter == 2 - assert q3.quarter == 3 - assert q4.quarter == 4 - - # end dates - q1 = arrow.Arrow(2013, 3, 31) - q2 = arrow.Arrow(2013, 6, 30) - q3 = arrow.Arrow(2013, 9, 30) - q4 = arrow.Arrow(2013, 12, 31) - assert q1.quarter == 1 - assert q2.quarter == 2 - assert q3.quarter == 3 - assert q4.quarter == 4 - - def test_getattr_dt_value(self): - - assert self.arrow.year == 2013 - - def test_tzinfo(self): - - self.arrow.tzinfo = tz.gettz("PST") - assert self.arrow.tzinfo == tz.gettz("PST") - - def test_naive(self): - - assert self.arrow.naive == self.arrow._datetime.replace(tzinfo=None) - - def test_timestamp(self): - - assert self.arrow.timestamp == calendar.timegm( - self.arrow._datetime.utctimetuple() - ) - - with pytest.warns(DeprecationWarning): - self.arrow.timestamp - - def test_int_timestamp(self): - - assert self.arrow.int_timestamp == calendar.timegm( - self.arrow._datetime.utctimetuple() - ) - - def test_float_timestamp(self): - - result = self.arrow.float_timestamp - self.arrow.timestamp - - assert result == self.arrow.microsecond - - def test_getattr_fold(self): - - # UTC is always unambiguous - assert self.now.fold == 0 - - ambiguous_dt = arrow.Arrow( - 2017, 10, 29, 2, 0, tzinfo="Europe/Stockholm", fold=1 - ) - assert ambiguous_dt.fold == 1 - - with pytest.raises(AttributeError): - ambiguous_dt.fold = 0 - - def test_getattr_ambiguous(self): - - assert not self.now.ambiguous - - ambiguous_dt = arrow.Arrow(2017, 10, 29, 2, 0, tzinfo="Europe/Stockholm") - - assert ambiguous_dt.ambiguous - - def test_getattr_imaginary(self): - - assert not self.now.imaginary - - imaginary_dt = arrow.Arrow(2013, 3, 31, 2, 30, tzinfo="Europe/Paris") - - assert imaginary_dt.imaginary - - -@pytest.mark.usefixtures("time_utcnow") -class TestArrowComparison: - def test_eq(self): - - assert self.arrow == self.arrow - assert self.arrow == self.arrow.datetime - assert not (self.arrow == "abc") - - def test_ne(self): - - assert not (self.arrow != self.arrow) - assert not (self.arrow != self.arrow.datetime) - assert self.arrow != "abc" - - def test_gt(self): - - arrow_cmp = self.arrow.shift(minutes=1) - - assert not (self.arrow > self.arrow) - assert not (self.arrow > self.arrow.datetime) - - with pytest.raises(TypeError): - self.arrow > "abc" - - assert self.arrow < arrow_cmp - assert self.arrow < arrow_cmp.datetime - - def test_ge(self): - - with pytest.raises(TypeError): - self.arrow >= "abc" - - assert self.arrow >= self.arrow - assert self.arrow >= self.arrow.datetime - - def test_lt(self): - - arrow_cmp = self.arrow.shift(minutes=1) - - assert not (self.arrow < self.arrow) - assert not (self.arrow < self.arrow.datetime) - - with pytest.raises(TypeError): - self.arrow < "abc" - - assert self.arrow < arrow_cmp - assert self.arrow < arrow_cmp.datetime - - def test_le(self): - - with pytest.raises(TypeError): - self.arrow <= "abc" - - assert self.arrow <= self.arrow - assert self.arrow <= self.arrow.datetime - - -@pytest.mark.usefixtures("time_2013_01_01") -class TestArrowMath: - def test_add_timedelta(self): - - result = self.arrow.__add__(timedelta(days=1)) - - assert result._datetime == datetime(2013, 1, 2, tzinfo=tz.tzutc()) - - def test_add_other(self): - - with pytest.raises(TypeError): - self.arrow + 1 - - def test_radd(self): - - result = self.arrow.__radd__(timedelta(days=1)) - - assert result._datetime == datetime(2013, 1, 2, tzinfo=tz.tzutc()) - - def test_sub_timedelta(self): - - result = self.arrow.__sub__(timedelta(days=1)) - - assert result._datetime == datetime(2012, 12, 31, tzinfo=tz.tzutc()) - - def test_sub_datetime(self): - - result = self.arrow.__sub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) - - assert result == timedelta(days=11) - - def test_sub_arrow(self): - - result = self.arrow.__sub__(arrow.Arrow(2012, 12, 21, tzinfo=tz.tzutc())) - - assert result == timedelta(days=11) - - def test_sub_other(self): - - with pytest.raises(TypeError): - self.arrow - object() - - def test_rsub_datetime(self): - - result = self.arrow.__rsub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) - - assert result == timedelta(days=-11) - - def test_rsub_other(self): - - with pytest.raises(TypeError): - timedelta(days=1) - self.arrow - - -@pytest.mark.usefixtures("time_utcnow") -class TestArrowDatetimeInterface: - def test_date(self): - - result = self.arrow.date() - - assert result == self.arrow._datetime.date() - - def test_time(self): - - result = self.arrow.time() - - assert result == self.arrow._datetime.time() - - def test_timetz(self): - - result = self.arrow.timetz() - - assert result == self.arrow._datetime.timetz() - - def test_astimezone(self): - - other_tz = tz.gettz("US/Pacific") - - result = self.arrow.astimezone(other_tz) - - assert result == self.arrow._datetime.astimezone(other_tz) - - def test_utcoffset(self): - - result = self.arrow.utcoffset() - - assert result == self.arrow._datetime.utcoffset() - - def test_dst(self): - - result = self.arrow.dst() - - assert result == self.arrow._datetime.dst() - - def test_timetuple(self): - - result = self.arrow.timetuple() - - assert result == self.arrow._datetime.timetuple() - - def test_utctimetuple(self): - - result = self.arrow.utctimetuple() - - assert result == self.arrow._datetime.utctimetuple() - - def test_toordinal(self): - - result = self.arrow.toordinal() - - assert result == self.arrow._datetime.toordinal() - - def test_weekday(self): - - result = self.arrow.weekday() - - assert result == self.arrow._datetime.weekday() - - def test_isoweekday(self): - - result = self.arrow.isoweekday() - - assert result == self.arrow._datetime.isoweekday() - - def test_isocalendar(self): - - result = self.arrow.isocalendar() - - assert result == self.arrow._datetime.isocalendar() - - def test_isoformat(self): - - result = self.arrow.isoformat() - - assert result == self.arrow._datetime.isoformat() - - def test_simplejson(self): - - result = json.dumps({"v": self.arrow.for_json()}, for_json=True) - - assert json.loads(result)["v"] == self.arrow._datetime.isoformat() - - def test_ctime(self): - - result = self.arrow.ctime() - - assert result == self.arrow._datetime.ctime() - - def test_strftime(self): - - result = self.arrow.strftime("%Y") - - assert result == self.arrow._datetime.strftime("%Y") - - -class TestArrowFalsePositiveDst: - """These tests relate to issues #376 and #551. - The key points in both issues are that arrow will assign a UTC timezone if none is provided and - .to() will change other attributes to be correct whereas .replace() only changes the specified attribute. - - Issue 376 - >>> arrow.get('2016-11-06').to('America/New_York').ceil('day') - < Arrow [2016-11-05T23:59:59.999999-04:00] > - - Issue 551 - >>> just_before = arrow.get('2018-11-04T01:59:59.999999') - >>> just_before - 2018-11-04T01:59:59.999999+00:00 - >>> just_after = just_before.shift(microseconds=1) - >>> just_after - 2018-11-04T02:00:00+00:00 - >>> just_before_eastern = just_before.replace(tzinfo='US/Eastern') - >>> just_before_eastern - 2018-11-04T01:59:59.999999-04:00 - >>> just_after_eastern = just_after.replace(tzinfo='US/Eastern') - >>> just_after_eastern - 2018-11-04T02:00:00-05:00 - """ - - def test_dst(self): - self.before_1 = arrow.Arrow( - 2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York") - ) - self.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) - self.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York")) - self.after_2 = arrow.Arrow( - 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - self.before_3 = arrow.Arrow( - 2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York") - ) - self.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) - self.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York")) - self.after_4 = arrow.Arrow( - 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - assert self.before_1.day == self.before_2.day - assert self.after_1.day == self.after_2.day - assert self.before_3.day == self.before_4.day - assert self.after_3.day == self.after_4.day - - -class TestArrowConversion: - def test_to(self): - - dt_from = datetime.now() - arrow_from = arrow.Arrow.fromdatetime(dt_from, tz.gettz("US/Pacific")) - - self.expected = dt_from.replace(tzinfo=tz.gettz("US/Pacific")).astimezone( - tz.tzutc() - ) - - assert arrow_from.to("UTC").datetime == self.expected - assert arrow_from.to(tz.tzutc()).datetime == self.expected - - # issue #368 - def test_to_pacific_then_utc(self): - result = arrow.Arrow(2018, 11, 4, 1, tzinfo="-08:00").to("US/Pacific").to("UTC") - assert result == arrow.Arrow(2018, 11, 4, 9) - - # issue #368 - def test_to_amsterdam_then_utc(self): - result = arrow.Arrow(2016, 10, 30).to("Europe/Amsterdam") - assert result.utcoffset() == timedelta(seconds=7200) - - # regression test for #690 - def test_to_israel_same_offset(self): - - result = arrow.Arrow(2019, 10, 27, 2, 21, 1, tzinfo="+03:00").to("Israel") - expected = arrow.Arrow(2019, 10, 27, 1, 21, 1, tzinfo="Israel") - - assert result == expected - assert result.utcoffset() != expected.utcoffset() - - # issue 315 - def test_anchorage_dst(self): - before = arrow.Arrow(2016, 3, 13, 1, 59, tzinfo="America/Anchorage") - after = arrow.Arrow(2016, 3, 13, 2, 1, tzinfo="America/Anchorage") - - assert before.utcoffset() != after.utcoffset() - - # issue 476 - def test_chicago_fall(self): - - result = arrow.Arrow(2017, 11, 5, 2, 1, tzinfo="-05:00").to("America/Chicago") - expected = arrow.Arrow(2017, 11, 5, 1, 1, tzinfo="America/Chicago") - - assert result == expected - assert result.utcoffset() != expected.utcoffset() - - def test_toronto_gap(self): - - before = arrow.Arrow(2011, 3, 13, 6, 30, tzinfo="UTC").to("America/Toronto") - after = arrow.Arrow(2011, 3, 13, 7, 30, tzinfo="UTC").to("America/Toronto") - - assert before.datetime.replace(tzinfo=None) == datetime(2011, 3, 13, 1, 30) - assert after.datetime.replace(tzinfo=None) == datetime(2011, 3, 13, 3, 30) - - assert before.utcoffset() != after.utcoffset() - - def test_sydney_gap(self): - - before = arrow.Arrow(2012, 10, 6, 15, 30, tzinfo="UTC").to("Australia/Sydney") - after = arrow.Arrow(2012, 10, 6, 16, 30, tzinfo="UTC").to("Australia/Sydney") - - assert before.datetime.replace(tzinfo=None) == datetime(2012, 10, 7, 1, 30) - assert after.datetime.replace(tzinfo=None) == datetime(2012, 10, 7, 3, 30) - - assert before.utcoffset() != after.utcoffset() - - -class TestArrowPickling: - def test_pickle_and_unpickle(self): - - dt = arrow.Arrow.utcnow() - - pickled = pickle.dumps(dt) - - unpickled = pickle.loads(pickled) - - assert unpickled == dt - - -class TestArrowReplace: - def test_not_attr(self): - - with pytest.raises(AttributeError): - arrow.Arrow.utcnow().replace(abc=1) - - def test_replace(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - assert arw.replace(year=2012) == arrow.Arrow(2012, 5, 5, 12, 30, 45) - assert arw.replace(month=1) == arrow.Arrow(2013, 1, 5, 12, 30, 45) - assert arw.replace(day=1) == arrow.Arrow(2013, 5, 1, 12, 30, 45) - assert arw.replace(hour=1) == arrow.Arrow(2013, 5, 5, 1, 30, 45) - assert arw.replace(minute=1) == arrow.Arrow(2013, 5, 5, 12, 1, 45) - assert arw.replace(second=1) == arrow.Arrow(2013, 5, 5, 12, 30, 1) - - def test_replace_tzinfo(self): - - arw = arrow.Arrow.utcnow().to("US/Eastern") - - result = arw.replace(tzinfo=tz.gettz("US/Pacific")) - - assert result == arw.datetime.replace(tzinfo=tz.gettz("US/Pacific")) - - def test_replace_fold(self): - - before = arrow.Arrow(2017, 11, 5, 1, tzinfo="America/New_York") - after = before.replace(fold=1) - - assert before.fold == 0 - assert after.fold == 1 - assert before == after - assert before.utcoffset() != after.utcoffset() - - def test_replace_fold_and_other(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - assert arw.replace(fold=1, minute=50) == arrow.Arrow(2013, 5, 5, 12, 50, 45) - assert arw.replace(minute=50, fold=1) == arrow.Arrow(2013, 5, 5, 12, 50, 45) - - def test_replace_week(self): - - with pytest.raises(AttributeError): - arrow.Arrow.utcnow().replace(week=1) - - def test_replace_quarter(self): - - with pytest.raises(AttributeError): - arrow.Arrow.utcnow().replace(quarter=1) - - def test_replace_quarter_and_fold(self): - with pytest.raises(AttributeError): - arrow.utcnow().replace(fold=1, quarter=1) - - with pytest.raises(AttributeError): - arrow.utcnow().replace(quarter=1, fold=1) - - def test_replace_other_kwargs(self): - - with pytest.raises(AttributeError): - arrow.utcnow().replace(abc="def") - - -class TestArrowShift: - def test_not_attr(self): - - now = arrow.Arrow.utcnow() - - with pytest.raises(AttributeError): - now.shift(abc=1) - - with pytest.raises(AttributeError): - now.shift(week=1) - - def test_shift(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - assert arw.shift(years=1) == arrow.Arrow(2014, 5, 5, 12, 30, 45) - assert arw.shift(quarters=1) == arrow.Arrow(2013, 8, 5, 12, 30, 45) - assert arw.shift(quarters=1, months=1) == arrow.Arrow(2013, 9, 5, 12, 30, 45) - assert arw.shift(months=1) == arrow.Arrow(2013, 6, 5, 12, 30, 45) - assert arw.shift(weeks=1) == arrow.Arrow(2013, 5, 12, 12, 30, 45) - assert arw.shift(days=1) == arrow.Arrow(2013, 5, 6, 12, 30, 45) - assert arw.shift(hours=1) == arrow.Arrow(2013, 5, 5, 13, 30, 45) - assert arw.shift(minutes=1) == arrow.Arrow(2013, 5, 5, 12, 31, 45) - assert arw.shift(seconds=1) == arrow.Arrow(2013, 5, 5, 12, 30, 46) - assert arw.shift(microseconds=1) == arrow.Arrow(2013, 5, 5, 12, 30, 45, 1) - - # Remember: Python's weekday 0 is Monday - assert arw.shift(weekday=0) == arrow.Arrow(2013, 5, 6, 12, 30, 45) - assert arw.shift(weekday=1) == arrow.Arrow(2013, 5, 7, 12, 30, 45) - assert arw.shift(weekday=2) == arrow.Arrow(2013, 5, 8, 12, 30, 45) - assert arw.shift(weekday=3) == arrow.Arrow(2013, 5, 9, 12, 30, 45) - assert arw.shift(weekday=4) == arrow.Arrow(2013, 5, 10, 12, 30, 45) - assert arw.shift(weekday=5) == arrow.Arrow(2013, 5, 11, 12, 30, 45) - assert arw.shift(weekday=6) == arw - - with pytest.raises(IndexError): - arw.shift(weekday=7) - - # Use dateutil.relativedelta's convenient day instances - assert arw.shift(weekday=MO) == arrow.Arrow(2013, 5, 6, 12, 30, 45) - assert arw.shift(weekday=MO(0)) == arrow.Arrow(2013, 5, 6, 12, 30, 45) - assert arw.shift(weekday=MO(1)) == arrow.Arrow(2013, 5, 6, 12, 30, 45) - assert arw.shift(weekday=MO(2)) == arrow.Arrow(2013, 5, 13, 12, 30, 45) - assert arw.shift(weekday=TU) == arrow.Arrow(2013, 5, 7, 12, 30, 45) - assert arw.shift(weekday=TU(0)) == arrow.Arrow(2013, 5, 7, 12, 30, 45) - assert arw.shift(weekday=TU(1)) == arrow.Arrow(2013, 5, 7, 12, 30, 45) - assert arw.shift(weekday=TU(2)) == arrow.Arrow(2013, 5, 14, 12, 30, 45) - assert arw.shift(weekday=WE) == arrow.Arrow(2013, 5, 8, 12, 30, 45) - assert arw.shift(weekday=WE(0)) == arrow.Arrow(2013, 5, 8, 12, 30, 45) - assert arw.shift(weekday=WE(1)) == arrow.Arrow(2013, 5, 8, 12, 30, 45) - assert arw.shift(weekday=WE(2)) == arrow.Arrow(2013, 5, 15, 12, 30, 45) - assert arw.shift(weekday=TH) == arrow.Arrow(2013, 5, 9, 12, 30, 45) - assert arw.shift(weekday=TH(0)) == arrow.Arrow(2013, 5, 9, 12, 30, 45) - assert arw.shift(weekday=TH(1)) == arrow.Arrow(2013, 5, 9, 12, 30, 45) - assert arw.shift(weekday=TH(2)) == arrow.Arrow(2013, 5, 16, 12, 30, 45) - assert arw.shift(weekday=FR) == arrow.Arrow(2013, 5, 10, 12, 30, 45) - assert arw.shift(weekday=FR(0)) == arrow.Arrow(2013, 5, 10, 12, 30, 45) - assert arw.shift(weekday=FR(1)) == arrow.Arrow(2013, 5, 10, 12, 30, 45) - assert arw.shift(weekday=FR(2)) == arrow.Arrow(2013, 5, 17, 12, 30, 45) - assert arw.shift(weekday=SA) == arrow.Arrow(2013, 5, 11, 12, 30, 45) - assert arw.shift(weekday=SA(0)) == arrow.Arrow(2013, 5, 11, 12, 30, 45) - assert arw.shift(weekday=SA(1)) == arrow.Arrow(2013, 5, 11, 12, 30, 45) - assert arw.shift(weekday=SA(2)) == arrow.Arrow(2013, 5, 18, 12, 30, 45) - assert arw.shift(weekday=SU) == arw - assert arw.shift(weekday=SU(0)) == arw - assert arw.shift(weekday=SU(1)) == arw - assert arw.shift(weekday=SU(2)) == arrow.Arrow(2013, 5, 12, 12, 30, 45) - - def test_shift_negative(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - assert arw.shift(years=-1) == arrow.Arrow(2012, 5, 5, 12, 30, 45) - assert arw.shift(quarters=-1) == arrow.Arrow(2013, 2, 5, 12, 30, 45) - assert arw.shift(quarters=-1, months=-1) == arrow.Arrow(2013, 1, 5, 12, 30, 45) - assert arw.shift(months=-1) == arrow.Arrow(2013, 4, 5, 12, 30, 45) - assert arw.shift(weeks=-1) == arrow.Arrow(2013, 4, 28, 12, 30, 45) - assert arw.shift(days=-1) == arrow.Arrow(2013, 5, 4, 12, 30, 45) - assert arw.shift(hours=-1) == arrow.Arrow(2013, 5, 5, 11, 30, 45) - assert arw.shift(minutes=-1) == arrow.Arrow(2013, 5, 5, 12, 29, 45) - assert arw.shift(seconds=-1) == arrow.Arrow(2013, 5, 5, 12, 30, 44) - assert arw.shift(microseconds=-1) == arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999) - - # Not sure how practical these negative weekdays are - assert arw.shift(weekday=-1) == arw.shift(weekday=SU) - assert arw.shift(weekday=-2) == arw.shift(weekday=SA) - assert arw.shift(weekday=-3) == arw.shift(weekday=FR) - assert arw.shift(weekday=-4) == arw.shift(weekday=TH) - assert arw.shift(weekday=-5) == arw.shift(weekday=WE) - assert arw.shift(weekday=-6) == arw.shift(weekday=TU) - assert arw.shift(weekday=-7) == arw.shift(weekday=MO) - - with pytest.raises(IndexError): - arw.shift(weekday=-8) - - assert arw.shift(weekday=MO(-1)) == arrow.Arrow(2013, 4, 29, 12, 30, 45) - assert arw.shift(weekday=TU(-1)) == arrow.Arrow(2013, 4, 30, 12, 30, 45) - assert arw.shift(weekday=WE(-1)) == arrow.Arrow(2013, 5, 1, 12, 30, 45) - assert arw.shift(weekday=TH(-1)) == arrow.Arrow(2013, 5, 2, 12, 30, 45) - assert arw.shift(weekday=FR(-1)) == arrow.Arrow(2013, 5, 3, 12, 30, 45) - assert arw.shift(weekday=SA(-1)) == arrow.Arrow(2013, 5, 4, 12, 30, 45) - assert arw.shift(weekday=SU(-1)) == arw - assert arw.shift(weekday=SU(-2)) == arrow.Arrow(2013, 4, 28, 12, 30, 45) - - def test_shift_quarters_bug(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - # The value of the last-read argument was used instead of the ``quarters`` argument. - # Recall that the keyword argument dict, like all dicts, is unordered, so only certain - # combinations of arguments would exhibit this. - assert arw.shift(quarters=0, years=1) == arrow.Arrow(2014, 5, 5, 12, 30, 45) - assert arw.shift(quarters=0, months=1) == arrow.Arrow(2013, 6, 5, 12, 30, 45) - assert arw.shift(quarters=0, weeks=1) == arrow.Arrow(2013, 5, 12, 12, 30, 45) - assert arw.shift(quarters=0, days=1) == arrow.Arrow(2013, 5, 6, 12, 30, 45) - assert arw.shift(quarters=0, hours=1) == arrow.Arrow(2013, 5, 5, 13, 30, 45) - assert arw.shift(quarters=0, minutes=1) == arrow.Arrow(2013, 5, 5, 12, 31, 45) - assert arw.shift(quarters=0, seconds=1) == arrow.Arrow(2013, 5, 5, 12, 30, 46) - assert arw.shift(quarters=0, microseconds=1) == arrow.Arrow( - 2013, 5, 5, 12, 30, 45, 1 - ) - - def test_shift_positive_imaginary(self): - - # Avoid shifting into imaginary datetimes, take into account DST and other timezone changes. - - new_york = arrow.Arrow(2017, 3, 12, 1, 30, tzinfo="America/New_York") - assert new_york.shift(hours=+1) == arrow.Arrow( - 2017, 3, 12, 3, 30, tzinfo="America/New_York" - ) - - # pendulum example - paris = arrow.Arrow(2013, 3, 31, 1, 50, tzinfo="Europe/Paris") - assert paris.shift(minutes=+20) == arrow.Arrow( - 2013, 3, 31, 3, 10, tzinfo="Europe/Paris" - ) - - canberra = arrow.Arrow(2018, 10, 7, 1, 30, tzinfo="Australia/Canberra") - assert canberra.shift(hours=+1) == arrow.Arrow( - 2018, 10, 7, 3, 30, tzinfo="Australia/Canberra" - ) - - kiev = arrow.Arrow(2018, 3, 25, 2, 30, tzinfo="Europe/Kiev") - assert kiev.shift(hours=+1) == arrow.Arrow( - 2018, 3, 25, 4, 30, tzinfo="Europe/Kiev" - ) - - # Edge case, the entire day of 2011-12-30 is imaginary in this zone! - apia = arrow.Arrow(2011, 12, 29, 23, tzinfo="Pacific/Apia") - assert apia.shift(hours=+2) == arrow.Arrow( - 2011, 12, 31, 1, tzinfo="Pacific/Apia" - ) - - def test_shift_negative_imaginary(self): - - new_york = arrow.Arrow(2011, 3, 13, 3, 30, tzinfo="America/New_York") - assert new_york.shift(hours=-1) == arrow.Arrow( - 2011, 3, 13, 3, 30, tzinfo="America/New_York" - ) - assert new_york.shift(hours=-2) == arrow.Arrow( - 2011, 3, 13, 1, 30, tzinfo="America/New_York" - ) - - london = arrow.Arrow(2019, 3, 31, 2, tzinfo="Europe/London") - assert london.shift(hours=-1) == arrow.Arrow( - 2019, 3, 31, 2, tzinfo="Europe/London" - ) - assert london.shift(hours=-2) == arrow.Arrow( - 2019, 3, 31, 0, tzinfo="Europe/London" - ) - - # edge case, crossing the international dateline - apia = arrow.Arrow(2011, 12, 31, 1, tzinfo="Pacific/Apia") - assert apia.shift(hours=-2) == arrow.Arrow( - 2011, 12, 31, 23, tzinfo="Pacific/Apia" - ) - - @pytest.mark.skipif( - dateutil.__version__ < "2.7.1", reason="old tz database (2018d needed)" - ) - def test_shift_kiritimati(self): - # corrected 2018d tz database release, will fail in earlier versions - - kiritimati = arrow.Arrow(1994, 12, 30, 12, 30, tzinfo="Pacific/Kiritimati") - assert kiritimati.shift(days=+1) == arrow.Arrow( - 1995, 1, 1, 12, 30, tzinfo="Pacific/Kiritimati" - ) - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="unsupported before python 3.6" - ) - def shift_imaginary_seconds(self): - # offset has a seconds component - monrovia = arrow.Arrow(1972, 1, 6, 23, tzinfo="Africa/Monrovia") - assert monrovia.shift(hours=+1, minutes=+30) == arrow.Arrow( - 1972, 1, 7, 1, 14, 30, tzinfo="Africa/Monrovia" - ) - - -class TestArrowRange: - def test_year(self): - - result = list( - arrow.Arrow.range( - "year", datetime(2013, 1, 2, 3, 4, 5), datetime(2016, 4, 5, 6, 7, 8) - ) - ) - - assert result == [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2014, 1, 2, 3, 4, 5), - arrow.Arrow(2015, 1, 2, 3, 4, 5), - arrow.Arrow(2016, 1, 2, 3, 4, 5), - ] - - def test_quarter(self): - - result = list( - arrow.Arrow.range( - "quarter", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) - ) - ) - - assert result == [ - arrow.Arrow(2013, 2, 3, 4, 5, 6), - arrow.Arrow(2013, 5, 3, 4, 5, 6), - ] - - def test_month(self): - - result = list( - arrow.Arrow.range( - "month", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) - ) - ) - - assert result == [ - arrow.Arrow(2013, 2, 3, 4, 5, 6), - arrow.Arrow(2013, 3, 3, 4, 5, 6), - arrow.Arrow(2013, 4, 3, 4, 5, 6), - arrow.Arrow(2013, 5, 3, 4, 5, 6), - ] - - def test_week(self): - - result = list( - arrow.Arrow.range( - "week", datetime(2013, 9, 1, 2, 3, 4), datetime(2013, 10, 1, 2, 3, 4) - ) - ) - - assert result == [ - arrow.Arrow(2013, 9, 1, 2, 3, 4), - arrow.Arrow(2013, 9, 8, 2, 3, 4), - arrow.Arrow(2013, 9, 15, 2, 3, 4), - arrow.Arrow(2013, 9, 22, 2, 3, 4), - arrow.Arrow(2013, 9, 29, 2, 3, 4), - ] - - def test_day(self): - - result = list( - arrow.Arrow.range( - "day", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 5, 6, 7, 8) - ) - ) - - assert result == [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 3, 3, 4, 5), - arrow.Arrow(2013, 1, 4, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 3, 4, 5), - ] - - def test_hour(self): - - result = list( - arrow.Arrow.range( - "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 6, 7, 8) - ) - ) - - assert result == [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 4, 4, 5), - arrow.Arrow(2013, 1, 2, 5, 4, 5), - arrow.Arrow(2013, 1, 2, 6, 4, 5), - ] - - result = list( - arrow.Arrow.range( - "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 5) - ) - ) - - assert result == [arrow.Arrow(2013, 1, 2, 3, 4, 5)] - - def test_minute(self): - - result = list( - arrow.Arrow.range( - "minute", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 7, 8) - ) - ) - - assert result == [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 3, 5, 5), - arrow.Arrow(2013, 1, 2, 3, 6, 5), - arrow.Arrow(2013, 1, 2, 3, 7, 5), - ] - - def test_second(self): - - result = list( - arrow.Arrow.range( - "second", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 8) - ) - ) - - assert result == [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 3, 4, 6), - arrow.Arrow(2013, 1, 2, 3, 4, 7), - arrow.Arrow(2013, 1, 2, 3, 4, 8), - ] - - def test_arrow(self): - - result = list( - arrow.Arrow.range( - "day", - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 6, 7, 8), - ) - ) - - assert result == [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 3, 3, 4, 5), - arrow.Arrow(2013, 1, 4, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 3, 4, 5), - ] - - def test_naive_tz(self): - - result = arrow.Arrow.range( - "year", datetime(2013, 1, 2, 3), datetime(2016, 4, 5, 6), "US/Pacific" - ) - - for r in result: - assert r.tzinfo == tz.gettz("US/Pacific") - - def test_aware_same_tz(self): - - result = arrow.Arrow.range( - "day", - arrow.Arrow(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")), - arrow.Arrow(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), - ) - - for r in result: - assert r.tzinfo == tz.gettz("US/Pacific") - - def test_aware_different_tz(self): - - result = arrow.Arrow.range( - "day", - datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")), - datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), - ) - - for r in result: - assert r.tzinfo == tz.gettz("US/Eastern") - - def test_aware_tz(self): - - result = arrow.Arrow.range( - "day", - datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")), - datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), - tz=tz.gettz("US/Central"), - ) - - for r in result: - assert r.tzinfo == tz.gettz("US/Central") - - def test_imaginary(self): - # issue #72, avoid duplication in utc column - - before = arrow.Arrow(2018, 3, 10, 23, tzinfo="US/Pacific") - after = arrow.Arrow(2018, 3, 11, 4, tzinfo="US/Pacific") - - pacific_range = [t for t in arrow.Arrow.range("hour", before, after)] - utc_range = [t.to("utc") for t in arrow.Arrow.range("hour", before, after)] - - assert len(pacific_range) == len(set(pacific_range)) - assert len(utc_range) == len(set(utc_range)) - - def test_unsupported(self): - - with pytest.raises(AttributeError): - next(arrow.Arrow.range("abc", datetime.utcnow(), datetime.utcnow())) - - def test_range_over_months_ending_on_different_days(self): - # regression test for issue #842 - result = list(arrow.Arrow.range("month", datetime(2015, 1, 31), limit=4)) - assert result == [ - arrow.Arrow(2015, 1, 31), - arrow.Arrow(2015, 2, 28), - arrow.Arrow(2015, 3, 31), - arrow.Arrow(2015, 4, 30), - ] - - result = list(arrow.Arrow.range("month", datetime(2015, 1, 30), limit=3)) - assert result == [ - arrow.Arrow(2015, 1, 30), - arrow.Arrow(2015, 2, 28), - arrow.Arrow(2015, 3, 30), - ] - - result = list(arrow.Arrow.range("month", datetime(2015, 2, 28), limit=3)) - assert result == [ - arrow.Arrow(2015, 2, 28), - arrow.Arrow(2015, 3, 28), - arrow.Arrow(2015, 4, 28), - ] - - result = list(arrow.Arrow.range("month", datetime(2015, 3, 31), limit=3)) - assert result == [ - arrow.Arrow(2015, 3, 31), - arrow.Arrow(2015, 4, 30), - arrow.Arrow(2015, 5, 31), - ] - - def test_range_over_quarter_months_ending_on_different_days(self): - result = list(arrow.Arrow.range("quarter", datetime(2014, 11, 30), limit=3)) - assert result == [ - arrow.Arrow(2014, 11, 30), - arrow.Arrow(2015, 2, 28), - arrow.Arrow(2015, 5, 30), - ] - - def test_range_over_year_maintains_end_date_across_leap_year(self): - result = list(arrow.Arrow.range("year", datetime(2012, 2, 29), limit=5)) - assert result == [ - arrow.Arrow(2012, 2, 29), - arrow.Arrow(2013, 2, 28), - arrow.Arrow(2014, 2, 28), - arrow.Arrow(2015, 2, 28), - arrow.Arrow(2016, 2, 29), - ] - - -class TestArrowSpanRange: - def test_year(self): - - result = list( - arrow.Arrow.span_range("year", datetime(2013, 2, 1), datetime(2016, 3, 31)) - ) - - assert result == [ - ( - arrow.Arrow(2013, 1, 1), - arrow.Arrow(2013, 12, 31, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2014, 1, 1), - arrow.Arrow(2014, 12, 31, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2015, 1, 1), - arrow.Arrow(2015, 12, 31, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2016, 1, 1), - arrow.Arrow(2016, 12, 31, 23, 59, 59, 999999), - ), - ] - - def test_quarter(self): - - result = list( - arrow.Arrow.span_range( - "quarter", datetime(2013, 2, 2), datetime(2013, 5, 15) - ) - ) - - assert result == [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 6, 30, 23, 59, 59, 999999)), - ] - - def test_month(self): - - result = list( - arrow.Arrow.span_range("month", datetime(2013, 1, 2), datetime(2013, 4, 15)) - ) - - assert result == [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 1, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 1), arrow.Arrow(2013, 2, 28, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 3, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 4, 30, 23, 59, 59, 999999)), - ] - - def test_week(self): - - result = list( - arrow.Arrow.span_range("week", datetime(2013, 2, 2), datetime(2013, 2, 28)) - ) - - assert result == [ - (arrow.Arrow(2013, 1, 28), arrow.Arrow(2013, 2, 3, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 4), arrow.Arrow(2013, 2, 10, 23, 59, 59, 999999)), - ( - arrow.Arrow(2013, 2, 11), - arrow.Arrow(2013, 2, 17, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 2, 18), - arrow.Arrow(2013, 2, 24, 23, 59, 59, 999999), - ), - (arrow.Arrow(2013, 2, 25), arrow.Arrow(2013, 3, 3, 23, 59, 59, 999999)), - ] - - def test_day(self): - - result = list( - arrow.Arrow.span_range( - "day", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) - ) - ) - - assert result == [ - ( - arrow.Arrow(2013, 1, 1, 0), - arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 2, 0), - arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 3, 0), - arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 4, 0), - arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), - ), - ] - - def test_days(self): - - result = list( - arrow.Arrow.span_range( - "days", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) - ) - ) - - assert result == [ - ( - arrow.Arrow(2013, 1, 1, 0), - arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 2, 0), - arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 3, 0), - arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 4, 0), - arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), - ), - ] - - def test_hour(self): - - result = list( - arrow.Arrow.span_range( - "hour", datetime(2013, 1, 1, 0, 30), datetime(2013, 1, 1, 3, 30) - ) - ) - - assert result == [ - ( - arrow.Arrow(2013, 1, 1, 0), - arrow.Arrow(2013, 1, 1, 0, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 1), - arrow.Arrow(2013, 1, 1, 1, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 2), - arrow.Arrow(2013, 1, 1, 2, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 3), - arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999), - ), - ] - - result = list( - arrow.Arrow.span_range( - "hour", datetime(2013, 1, 1, 3, 30), datetime(2013, 1, 1, 3, 30) - ) - ) - - assert result == [ - (arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999)) - ] - - def test_minute(self): - - result = list( - arrow.Arrow.span_range( - "minute", datetime(2013, 1, 1, 0, 0, 30), datetime(2013, 1, 1, 0, 3, 30) - ) - ) - - assert result == [ - ( - arrow.Arrow(2013, 1, 1, 0, 0), - arrow.Arrow(2013, 1, 1, 0, 0, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 1), - arrow.Arrow(2013, 1, 1, 0, 1, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 2), - arrow.Arrow(2013, 1, 1, 0, 2, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 3), - arrow.Arrow(2013, 1, 1, 0, 3, 59, 999999), - ), - ] - - def test_second(self): - - result = list( - arrow.Arrow.span_range( - "second", datetime(2013, 1, 1), datetime(2013, 1, 1, 0, 0, 3) - ) - ) - - assert result == [ - ( - arrow.Arrow(2013, 1, 1, 0, 0, 0), - arrow.Arrow(2013, 1, 1, 0, 0, 0, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 0, 1), - arrow.Arrow(2013, 1, 1, 0, 0, 1, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 0, 2), - arrow.Arrow(2013, 1, 1, 0, 0, 2, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 0, 3), - arrow.Arrow(2013, 1, 1, 0, 0, 3, 999999), - ), - ] - - def test_naive_tz(self): - - tzinfo = tz.gettz("US/Pacific") - - result = arrow.Arrow.span_range( - "hour", datetime(2013, 1, 1, 0), datetime(2013, 1, 1, 3, 59), "US/Pacific" - ) - - for f, c in result: - assert f.tzinfo == tzinfo - assert c.tzinfo == tzinfo - - def test_aware_same_tz(self): - - tzinfo = tz.gettz("US/Pacific") - - result = arrow.Arrow.span_range( - "hour", - datetime(2013, 1, 1, 0, tzinfo=tzinfo), - datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo), - ) - - for f, c in result: - assert f.tzinfo == tzinfo - assert c.tzinfo == tzinfo - - def test_aware_different_tz(self): - - tzinfo1 = tz.gettz("US/Pacific") - tzinfo2 = tz.gettz("US/Eastern") - - result = arrow.Arrow.span_range( - "hour", - datetime(2013, 1, 1, 0, tzinfo=tzinfo1), - datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo2), - ) - - for f, c in result: - assert f.tzinfo == tzinfo1 - assert c.tzinfo == tzinfo1 - - def test_aware_tz(self): - - result = arrow.Arrow.span_range( - "hour", - datetime(2013, 1, 1, 0, tzinfo=tz.gettz("US/Eastern")), - datetime(2013, 1, 1, 2, 59, tzinfo=tz.gettz("US/Eastern")), - tz="US/Central", - ) - - for f, c in result: - assert f.tzinfo == tz.gettz("US/Central") - assert c.tzinfo == tz.gettz("US/Central") - - def test_bounds_param_is_passed(self): - - result = list( - arrow.Arrow.span_range( - "quarter", datetime(2013, 2, 2), datetime(2013, 5, 15), bounds="[]" - ) - ) - - assert result == [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 4, 1)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 7, 1)), - ] - - -class TestArrowInterval: - def test_incorrect_input(self): - with pytest.raises(ValueError): - list( - arrow.Arrow.interval( - "month", datetime(2013, 1, 2), datetime(2013, 4, 15), 0 - ) - ) - - def test_correct(self): - result = list( - arrow.Arrow.interval( - "hour", datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15), 2 - ) - ) - - assert result == [ - ( - arrow.Arrow(2013, 5, 5, 12), - arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 5, 5, 14), - arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 5, 5, 16), - arrow.Arrow(2013, 5, 5, 17, 59, 59, 999999), - ), - ] - - def test_bounds_param_is_passed(self): - result = list( - arrow.Arrow.interval( - "hour", - datetime(2013, 5, 5, 12, 30), - datetime(2013, 5, 5, 17, 15), - 2, - bounds="[]", - ) - ) - - assert result == [ - (arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 14)), - (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 16)), - (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 18)), - ] - - -@pytest.mark.usefixtures("time_2013_02_15") -class TestArrowSpan: - def test_span_attribute(self): - - with pytest.raises(AttributeError): - self.arrow.span("span") - - def test_span_year(self): - - floor, ceil = self.arrow.span("year") - - assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - - def test_span_quarter(self): - - floor, ceil = self.arrow.span("quarter") - - assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 3, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - - def test_span_quarter_count(self): - - floor, ceil = self.arrow.span("quarter", 2) - - assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 6, 30, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - - def test_span_year_count(self): - - floor, ceil = self.arrow.span("year", 2) - - assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) - assert ceil == datetime(2014, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - - def test_span_month(self): - - floor, ceil = self.arrow.span("month") - - assert floor == datetime(2013, 2, 1, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 28, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - - def test_span_week(self): - - floor, ceil = self.arrow.span("week") - - assert floor == datetime(2013, 2, 11, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - - def test_span_day(self): - - floor, ceil = self.arrow.span("day") - - assert floor == datetime(2013, 2, 15, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - - def test_span_hour(self): - - floor, ceil = self.arrow.span("hour") - - assert floor == datetime(2013, 2, 15, 3, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) - - def test_span_minute(self): - - floor, ceil = self.arrow.span("minute") - - assert floor == datetime(2013, 2, 15, 3, 41, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 15, 3, 41, 59, 999999, tzinfo=tz.tzutc()) - - def test_span_second(self): - - floor, ceil = self.arrow.span("second") - - assert floor == datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc()) - - def test_span_microsecond(self): - - floor, ceil = self.arrow.span("microsecond") - - assert floor == datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) - - def test_floor(self): - - floor, ceil = self.arrow.span("month") - - assert floor == self.arrow.floor("month") - assert ceil == self.arrow.ceil("month") - - def test_span_inclusive_inclusive(self): - - floor, ceil = self.arrow.span("hour", bounds="[]") - - assert floor == datetime(2013, 2, 15, 3, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 15, 4, tzinfo=tz.tzutc()) - - def test_span_exclusive_inclusive(self): - - floor, ceil = self.arrow.span("hour", bounds="(]") - - assert floor == datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 15, 4, tzinfo=tz.tzutc()) - - def test_span_exclusive_exclusive(self): - - floor, ceil = self.arrow.span("hour", bounds="()") - - assert floor == datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc()) - assert ceil == datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) - - def test_bounds_are_validated(self): - - with pytest.raises(ValueError): - floor, ceil = self.arrow.span("hour", bounds="][") - - -@pytest.mark.usefixtures("time_2013_01_01") -class TestArrowHumanize: - def test_granularity(self): - - assert self.now.humanize(granularity="second") == "just now" - - later1 = self.now.shift(seconds=1) - assert self.now.humanize(later1, granularity="second") == "just now" - assert later1.humanize(self.now, granularity="second") == "just now" - assert self.now.humanize(later1, granularity="minute") == "0 minutes ago" - assert later1.humanize(self.now, granularity="minute") == "in 0 minutes" - - later100 = self.now.shift(seconds=100) - assert self.now.humanize(later100, granularity="second") == "100 seconds ago" - assert later100.humanize(self.now, granularity="second") == "in 100 seconds" - assert self.now.humanize(later100, granularity="minute") == "a minute ago" - assert later100.humanize(self.now, granularity="minute") == "in a minute" - assert self.now.humanize(later100, granularity="hour") == "0 hours ago" - assert later100.humanize(self.now, granularity="hour") == "in 0 hours" - - later4000 = self.now.shift(seconds=4000) - assert self.now.humanize(later4000, granularity="minute") == "66 minutes ago" - assert later4000.humanize(self.now, granularity="minute") == "in 66 minutes" - assert self.now.humanize(later4000, granularity="hour") == "an hour ago" - assert later4000.humanize(self.now, granularity="hour") == "in an hour" - assert self.now.humanize(later4000, granularity="day") == "0 days ago" - assert later4000.humanize(self.now, granularity="day") == "in 0 days" - - later105 = self.now.shift(seconds=10 ** 5) - assert self.now.humanize(later105, granularity="hour") == "27 hours ago" - assert later105.humanize(self.now, granularity="hour") == "in 27 hours" - assert self.now.humanize(later105, granularity="day") == "a day ago" - assert later105.humanize(self.now, granularity="day") == "in a day" - assert self.now.humanize(later105, granularity="week") == "0 weeks ago" - assert later105.humanize(self.now, granularity="week") == "in 0 weeks" - assert self.now.humanize(later105, granularity="month") == "0 months ago" - assert later105.humanize(self.now, granularity="month") == "in 0 months" - assert self.now.humanize(later105, granularity=["month"]) == "0 months ago" - assert later105.humanize(self.now, granularity=["month"]) == "in 0 months" - - later106 = self.now.shift(seconds=3 * 10 ** 6) - assert self.now.humanize(later106, granularity="day") == "34 days ago" - assert later106.humanize(self.now, granularity="day") == "in 34 days" - assert self.now.humanize(later106, granularity="week") == "4 weeks ago" - assert later106.humanize(self.now, granularity="week") == "in 4 weeks" - assert self.now.humanize(later106, granularity="month") == "a month ago" - assert later106.humanize(self.now, granularity="month") == "in a month" - assert self.now.humanize(later106, granularity="year") == "0 years ago" - assert later106.humanize(self.now, granularity="year") == "in 0 years" - - later506 = self.now.shift(seconds=50 * 10 ** 6) - assert self.now.humanize(later506, granularity="week") == "82 weeks ago" - assert later506.humanize(self.now, granularity="week") == "in 82 weeks" - assert self.now.humanize(later506, granularity="month") == "18 months ago" - assert later506.humanize(self.now, granularity="month") == "in 18 months" - assert self.now.humanize(later506, granularity="year") == "a year ago" - assert later506.humanize(self.now, granularity="year") == "in a year" - - later108 = self.now.shift(seconds=10 ** 8) - assert self.now.humanize(later108, granularity="year") == "3 years ago" - assert later108.humanize(self.now, granularity="year") == "in 3 years" - - later108onlydistance = self.now.shift(seconds=10 ** 8) - assert ( - self.now.humanize( - later108onlydistance, only_distance=True, granularity="year" - ) - == "3 years" - ) - assert ( - later108onlydistance.humanize( - self.now, only_distance=True, granularity="year" - ) - == "3 years" - ) - - with pytest.raises(AttributeError): - self.now.humanize(later108, granularity="years") - - def test_multiple_granularity(self): - assert self.now.humanize(granularity="second") == "just now" - assert self.now.humanize(granularity=["second"]) == "just now" - assert ( - self.now.humanize(granularity=["year", "month", "day", "hour", "second"]) - == "in 0 years 0 months 0 days 0 hours and 0 seconds" - ) - - later4000 = self.now.shift(seconds=4000) - assert ( - later4000.humanize(self.now, granularity=["hour", "minute"]) - == "in an hour and 6 minutes" - ) - assert ( - self.now.humanize(later4000, granularity=["hour", "minute"]) - == "an hour and 6 minutes ago" - ) - assert ( - later4000.humanize( - self.now, granularity=["hour", "minute"], only_distance=True - ) - == "an hour and 6 minutes" - ) - assert ( - later4000.humanize(self.now, granularity=["day", "hour", "minute"]) - == "in 0 days an hour and 6 minutes" - ) - assert ( - self.now.humanize(later4000, granularity=["day", "hour", "minute"]) - == "0 days an hour and 6 minutes ago" - ) - - later105 = self.now.shift(seconds=10 ** 5) - assert ( - self.now.humanize(later105, granularity=["hour", "day", "minute"]) - == "a day 3 hours and 46 minutes ago" - ) - with pytest.raises(AttributeError): - self.now.humanize(later105, granularity=["error", "second"]) - - later108onlydistance = self.now.shift(seconds=10 ** 8) - assert ( - self.now.humanize( - later108onlydistance, only_distance=True, granularity=["year"] - ) - == "3 years" - ) - assert ( - self.now.humanize( - later108onlydistance, only_distance=True, granularity=["month", "week"] - ) - == "37 months and 4 weeks" - ) - assert ( - self.now.humanize( - later108onlydistance, only_distance=True, granularity=["year", "second"] - ) - == "3 years and 5327200 seconds" - ) - - one_min_one_sec_ago = self.now.shift(minutes=-1, seconds=-1) - assert ( - one_min_one_sec_ago.humanize(self.now, granularity=["minute", "second"]) - == "a minute and a second ago" - ) - - one_min_two_secs_ago = self.now.shift(minutes=-1, seconds=-2) - assert ( - one_min_two_secs_ago.humanize(self.now, granularity=["minute", "second"]) - == "a minute and 2 seconds ago" - ) - - def test_seconds(self): - - later = self.now.shift(seconds=10) - - # regression test for issue #727 - assert self.now.humanize(later) == "10 seconds ago" - assert later.humanize(self.now) == "in 10 seconds" - - assert self.now.humanize(later, only_distance=True) == "10 seconds" - assert later.humanize(self.now, only_distance=True) == "10 seconds" - - def test_minute(self): - - later = self.now.shift(minutes=1) - - assert self.now.humanize(later) == "a minute ago" - assert later.humanize(self.now) == "in a minute" - - assert self.now.humanize(later, only_distance=True) == "a minute" - assert later.humanize(self.now, only_distance=True) == "a minute" - - def test_minutes(self): - - later = self.now.shift(minutes=2) - - assert self.now.humanize(later) == "2 minutes ago" - assert later.humanize(self.now) == "in 2 minutes" - - assert self.now.humanize(later, only_distance=True) == "2 minutes" - assert later.humanize(self.now, only_distance=True) == "2 minutes" - - def test_hour(self): - - later = self.now.shift(hours=1) - - assert self.now.humanize(later) == "an hour ago" - assert later.humanize(self.now) == "in an hour" - - assert self.now.humanize(later, only_distance=True) == "an hour" - assert later.humanize(self.now, only_distance=True) == "an hour" - - def test_hours(self): - - later = self.now.shift(hours=2) - - assert self.now.humanize(later) == "2 hours ago" - assert later.humanize(self.now) == "in 2 hours" - - assert self.now.humanize(later, only_distance=True) == "2 hours" - assert later.humanize(self.now, only_distance=True) == "2 hours" - - def test_day(self): - - later = self.now.shift(days=1) - - assert self.now.humanize(later) == "a day ago" - assert later.humanize(self.now) == "in a day" - - # regression test for issue #697 - less_than_48_hours = self.now.shift( - days=1, hours=23, seconds=59, microseconds=999999 - ) - assert self.now.humanize(less_than_48_hours) == "a day ago" - assert less_than_48_hours.humanize(self.now) == "in a day" - - less_than_48_hours_date = less_than_48_hours._datetime.date() - with pytest.raises(TypeError): - # humanize other argument does not take raw datetime.date objects - self.now.humanize(less_than_48_hours_date) - - # convert from date to arrow object - less_than_48_hours_date = arrow.Arrow.fromdate(less_than_48_hours_date) - assert self.now.humanize(less_than_48_hours_date) == "a day ago" - assert less_than_48_hours_date.humanize(self.now) == "in a day" - - assert self.now.humanize(later, only_distance=True) == "a day" - assert later.humanize(self.now, only_distance=True) == "a day" - - def test_days(self): - - later = self.now.shift(days=2) - - assert self.now.humanize(later) == "2 days ago" - assert later.humanize(self.now) == "in 2 days" - - assert self.now.humanize(later, only_distance=True) == "2 days" - assert later.humanize(self.now, only_distance=True) == "2 days" - - # Regression tests for humanize bug referenced in issue 541 - later = self.now.shift(days=3) - assert later.humanize(self.now) == "in 3 days" - - later = self.now.shift(days=3, seconds=1) - assert later.humanize(self.now) == "in 3 days" - - later = self.now.shift(days=4) - assert later.humanize(self.now) == "in 4 days" - - def test_week(self): - - later = self.now.shift(weeks=1) - - assert self.now.humanize(later) == "a week ago" - assert later.humanize(self.now) == "in a week" - - assert self.now.humanize(later, only_distance=True) == "a week" - assert later.humanize(self.now, only_distance=True) == "a week" - - def test_weeks(self): - - later = self.now.shift(weeks=2) - - assert self.now.humanize(later) == "2 weeks ago" - assert later.humanize(self.now) == "in 2 weeks" - - assert self.now.humanize(later, only_distance=True) == "2 weeks" - assert later.humanize(self.now, only_distance=True) == "2 weeks" - - def test_month(self): - - later = self.now.shift(months=1) - - assert self.now.humanize(later) == "a month ago" - assert later.humanize(self.now) == "in a month" - - assert self.now.humanize(later, only_distance=True) == "a month" - assert later.humanize(self.now, only_distance=True) == "a month" - - def test_months(self): - - later = self.now.shift(months=2) - earlier = self.now.shift(months=-2) - - assert earlier.humanize(self.now) == "2 months ago" - assert later.humanize(self.now) == "in 2 months" - - assert self.now.humanize(later, only_distance=True) == "2 months" - assert later.humanize(self.now, only_distance=True) == "2 months" - - def test_year(self): - - later = self.now.shift(years=1) - - assert self.now.humanize(later) == "a year ago" - assert later.humanize(self.now) == "in a year" - - assert self.now.humanize(later, only_distance=True) == "a year" - assert later.humanize(self.now, only_distance=True) == "a year" - - def test_years(self): - - later = self.now.shift(years=2) - - assert self.now.humanize(later) == "2 years ago" - assert later.humanize(self.now) == "in 2 years" - - assert self.now.humanize(later, only_distance=True) == "2 years" - assert later.humanize(self.now, only_distance=True) == "2 years" - - arw = arrow.Arrow(2014, 7, 2) - - result = arw.humanize(self.datetime) - - assert result == "in 2 years" - - def test_arrow(self): - - arw = arrow.Arrow.fromdatetime(self.datetime) - - result = arw.humanize(arrow.Arrow.fromdatetime(self.datetime)) - - assert result == "just now" - - def test_datetime_tzinfo(self): - - arw = arrow.Arrow.fromdatetime(self.datetime) - - result = arw.humanize(self.datetime.replace(tzinfo=tz.tzutc())) - - assert result == "just now" - - def test_other(self): - - arw = arrow.Arrow.fromdatetime(self.datetime) - - with pytest.raises(TypeError): - arw.humanize(object()) - - def test_invalid_locale(self): - - arw = arrow.Arrow.fromdatetime(self.datetime) - - with pytest.raises(ValueError): - arw.humanize(locale="klingon") - - def test_none(self): - - arw = arrow.Arrow.utcnow() - - result = arw.humanize() - - assert result == "just now" - - result = arw.humanize(None) - - assert result == "just now" - - def test_untranslated_granularity(self, mocker): - - arw = arrow.Arrow.utcnow() - later = arw.shift(weeks=1) - - # simulate an untranslated timeframe key - mocker.patch.dict("arrow.locales.EnglishLocale.timeframes") - del arrow.locales.EnglishLocale.timeframes["week"] - with pytest.raises(ValueError): - arw.humanize(later, granularity="week") - - -@pytest.mark.usefixtures("time_2013_01_01") -class TestArrowHumanizeTestsWithLocale: - def test_now(self): - - arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) - - result = arw.humanize(self.datetime, locale="ru") - - assert result == "сейчас" - - def test_seconds(self): - arw = arrow.Arrow(2013, 1, 1, 0, 0, 44) - - result = arw.humanize(self.datetime, locale="ru") - - assert result == "через 44 несколько секунд" - - def test_years(self): - - arw = arrow.Arrow(2011, 7, 2) - - result = arw.humanize(self.datetime, locale="ru") - - assert result == "2 года назад" - - -class TestArrowIsBetween: - def test_start_before_end(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) - result = target.is_between(start, end) - assert not result - - def test_exclusive_exclusive_bounds(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 27)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 10)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 36)) - result = target.is_between(start, end, "()") - assert result - result = target.is_between(start, end) - assert result - - def test_exclusive_exclusive_bounds_same_date(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, "()") - assert not result - - def test_inclusive_exclusive_bounds(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 4)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) - result = target.is_between(start, end, "[)") - assert not result - - def test_exclusive_inclusive_bounds(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, "(]") - assert result - - def test_inclusive_inclusive_bounds_same_date(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, "[]") - assert result - - def test_type_error_exception(self): - with pytest.raises(TypeError): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = datetime(2013, 5, 5) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) - target.is_between(start, end) - - with pytest.raises(TypeError): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) - end = datetime(2013, 5, 8) - target.is_between(start, end) - - with pytest.raises(TypeError): - target.is_between(None, None) - - def test_value_error_exception(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) - with pytest.raises(ValueError): - target.is_between(start, end, "][") - with pytest.raises(ValueError): - target.is_between(start, end, "") - with pytest.raises(ValueError): - target.is_between(start, end, "]") - with pytest.raises(ValueError): - target.is_between(start, end, "[") - with pytest.raises(ValueError): - target.is_between(start, end, "hello") - - -class TestArrowUtil: - def test_get_datetime(self): - - get_datetime = arrow.Arrow._get_datetime - - arw = arrow.Arrow.utcnow() - dt = datetime.utcnow() - timestamp = time.time() - - assert get_datetime(arw) == arw.datetime - assert get_datetime(dt) == dt - assert ( - get_datetime(timestamp) == arrow.Arrow.utcfromtimestamp(timestamp).datetime - ) - - with pytest.raises(ValueError) as raise_ctx: - get_datetime("abc") - assert "not recognized as a datetime or timestamp" in str(raise_ctx.value) - - def test_get_tzinfo(self): - - get_tzinfo = arrow.Arrow._get_tzinfo - - with pytest.raises(ValueError) as raise_ctx: - get_tzinfo("abc") - assert "not recognized as a timezone" in str(raise_ctx.value) - - def test_get_iteration_params(self): - - assert arrow.Arrow._get_iteration_params("end", None) == ("end", sys.maxsize) - assert arrow.Arrow._get_iteration_params(None, 100) == (arrow.Arrow.max, 100) - assert arrow.Arrow._get_iteration_params(100, 120) == (100, 120) - - with pytest.raises(ValueError): - arrow.Arrow._get_iteration_params(None, None) diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_factory.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/test_factory.py deleted file mode 100644 index 2b8df5168f..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_factory.py +++ /dev/null @@ -1,390 +0,0 @@ -# -*- coding: utf-8 -*- -import time -from datetime import date, datetime - -import pytest -from dateutil import tz - -from arrow.parser import ParserError - -from .utils import assert_datetime_equality - - -@pytest.mark.usefixtures("arrow_factory") -class TestGet: - def test_no_args(self): - - assert_datetime_equality( - self.factory.get(), datetime.utcnow().replace(tzinfo=tz.tzutc()) - ) - - def test_timestamp_one_arg_no_arg(self): - - no_arg = self.factory.get(1406430900).timestamp - one_arg = self.factory.get("1406430900", "X").timestamp - - assert no_arg == one_arg - - def test_one_arg_none(self): - - assert_datetime_equality( - self.factory.get(None), datetime.utcnow().replace(tzinfo=tz.tzutc()) - ) - - def test_struct_time(self): - - assert_datetime_equality( - self.factory.get(time.gmtime()), - datetime.utcnow().replace(tzinfo=tz.tzutc()), - ) - - def test_one_arg_timestamp(self): - - int_timestamp = int(time.time()) - timestamp_dt = datetime.utcfromtimestamp(int_timestamp).replace( - tzinfo=tz.tzutc() - ) - - assert self.factory.get(int_timestamp) == timestamp_dt - - with pytest.raises(ParserError): - self.factory.get(str(int_timestamp)) - - float_timestamp = time.time() - timestamp_dt = datetime.utcfromtimestamp(float_timestamp).replace( - tzinfo=tz.tzutc() - ) - - assert self.factory.get(float_timestamp) == timestamp_dt - - with pytest.raises(ParserError): - self.factory.get(str(float_timestamp)) - - # Regression test for issue #216 - # Python 3 raises OverflowError, Python 2 raises ValueError - timestamp = 99999999999999999999999999.99999999999999999999999999 - with pytest.raises((OverflowError, ValueError)): - self.factory.get(timestamp) - - def test_one_arg_expanded_timestamp(self): - - millisecond_timestamp = 1591328104308 - microsecond_timestamp = 1591328104308505 - - # Regression test for issue #796 - assert self.factory.get(millisecond_timestamp) == datetime.utcfromtimestamp( - 1591328104.308 - ).replace(tzinfo=tz.tzutc()) - assert self.factory.get(microsecond_timestamp) == datetime.utcfromtimestamp( - 1591328104.308505 - ).replace(tzinfo=tz.tzutc()) - - def test_one_arg_timestamp_with_tzinfo(self): - - timestamp = time.time() - timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone( - tz.gettz("US/Pacific") - ) - timezone = tz.gettz("US/Pacific") - - assert_datetime_equality( - self.factory.get(timestamp, tzinfo=timezone), timestamp_dt - ) - - def test_one_arg_arrow(self): - - arw = self.factory.utcnow() - result = self.factory.get(arw) - - assert arw == result - - def test_one_arg_datetime(self): - - dt = datetime.utcnow().replace(tzinfo=tz.tzutc()) - - assert self.factory.get(dt) == dt - - def test_one_arg_date(self): - - d = date.today() - dt = datetime(d.year, d.month, d.day, tzinfo=tz.tzutc()) - - assert self.factory.get(d) == dt - - def test_one_arg_tzinfo(self): - - self.expected = ( - datetime.utcnow() - .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) - ) - - assert_datetime_equality( - self.factory.get(tz.gettz("US/Pacific")), self.expected - ) - - # regression test for issue #658 - def test_one_arg_dateparser_datetime(self): - dateparser = pytest.importorskip("dateparser") - expected = datetime(1990, 1, 1).replace(tzinfo=tz.tzutc()) - # dateparser outputs: datetime.datetime(1990, 1, 1, 0, 0, tzinfo=) - parsed_date = dateparser.parse("1990-01-01T00:00:00+00:00") - dt_output = self.factory.get(parsed_date)._datetime.replace(tzinfo=tz.tzutc()) - assert dt_output == expected - - def test_kwarg_tzinfo(self): - - self.expected = ( - datetime.utcnow() - .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) - ) - - assert_datetime_equality( - self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected - ) - - def test_kwarg_tzinfo_string(self): - - self.expected = ( - datetime.utcnow() - .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) - ) - - assert_datetime_equality(self.factory.get(tzinfo="US/Pacific"), self.expected) - - with pytest.raises(ParserError): - self.factory.get(tzinfo="US/PacificInvalidTzinfo") - - def test_kwarg_normalize_whitespace(self): - result = self.factory.get( - "Jun 1 2005 1:33PM", - "MMM D YYYY H:mmA", - tzinfo=tz.tzutc(), - normalize_whitespace=True, - ) - assert result._datetime == datetime(2005, 6, 1, 13, 33, tzinfo=tz.tzutc()) - - result = self.factory.get( - "\t 2013-05-05T12:30:45.123456 \t \n", - tzinfo=tz.tzutc(), - normalize_whitespace=True, - ) - assert result._datetime == datetime( - 2013, 5, 5, 12, 30, 45, 123456, tzinfo=tz.tzutc() - ) - - def test_one_arg_iso_str(self): - - dt = datetime.utcnow() - - assert_datetime_equality( - self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc()) - ) - - def test_one_arg_iso_calendar(self): - - pairs = [ - (datetime(2004, 1, 4), (2004, 1, 7)), - (datetime(2008, 12, 30), (2009, 1, 2)), - (datetime(2010, 1, 2), (2009, 53, 6)), - (datetime(2000, 2, 29), (2000, 9, 2)), - (datetime(2005, 1, 1), (2004, 53, 6)), - (datetime(2010, 1, 4), (2010, 1, 1)), - (datetime(2010, 1, 3), (2009, 53, 7)), - (datetime(2003, 12, 29), (2004, 1, 1)), - ] - - for pair in pairs: - dt, iso = pair - assert self.factory.get(iso) == self.factory.get(dt) - - with pytest.raises(TypeError): - self.factory.get((2014, 7, 1, 4)) - - with pytest.raises(TypeError): - self.factory.get((2014, 7)) - - with pytest.raises(ValueError): - self.factory.get((2014, 70, 1)) - - with pytest.raises(ValueError): - self.factory.get((2014, 7, 10)) - - def test_one_arg_other(self): - - with pytest.raises(TypeError): - self.factory.get(object()) - - def test_one_arg_bool(self): - - with pytest.raises(TypeError): - self.factory.get(False) - - with pytest.raises(TypeError): - self.factory.get(True) - - def test_two_args_datetime_tzinfo(self): - - result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific")) - - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) - - def test_two_args_datetime_tz_str(self): - - result = self.factory.get(datetime(2013, 1, 1), "US/Pacific") - - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) - - def test_two_args_date_tzinfo(self): - - result = self.factory.get(date(2013, 1, 1), tz.gettz("US/Pacific")) - - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) - - def test_two_args_date_tz_str(self): - - result = self.factory.get(date(2013, 1, 1), "US/Pacific") - - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) - - def test_two_args_datetime_other(self): - - with pytest.raises(TypeError): - self.factory.get(datetime.utcnow(), object()) - - def test_two_args_date_other(self): - - with pytest.raises(TypeError): - self.factory.get(date.today(), object()) - - def test_two_args_str_str(self): - - result = self.factory.get("2013-01-01", "YYYY-MM-DD") - - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) - - def test_two_args_str_tzinfo(self): - - result = self.factory.get("2013-01-01", tzinfo=tz.gettz("US/Pacific")) - - assert_datetime_equality( - result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) - ) - - def test_two_args_twitter_format(self): - - # format returned by twitter API for created_at: - twitter_date = "Fri Apr 08 21:08:54 +0000 2016" - result = self.factory.get(twitter_date, "ddd MMM DD HH:mm:ss Z YYYY") - - assert result._datetime == datetime(2016, 4, 8, 21, 8, 54, tzinfo=tz.tzutc()) - - def test_two_args_str_list(self): - - result = self.factory.get("2013-01-01", ["MM/DD/YYYY", "YYYY-MM-DD"]) - - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) - - def test_two_args_unicode_unicode(self): - - result = self.factory.get(u"2013-01-01", u"YYYY-MM-DD") - - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) - - def test_two_args_other(self): - - with pytest.raises(TypeError): - self.factory.get(object(), object()) - - def test_three_args_with_tzinfo(self): - - timefmt = "YYYYMMDD" - d = "20150514" - - assert self.factory.get(d, timefmt, tzinfo=tz.tzlocal()) == datetime( - 2015, 5, 14, tzinfo=tz.tzlocal() - ) - - def test_three_args(self): - - assert self.factory.get(2013, 1, 1) == datetime(2013, 1, 1, tzinfo=tz.tzutc()) - - def test_full_kwargs(self): - - assert ( - self.factory.get( - year=2016, - month=7, - day=14, - hour=7, - minute=16, - second=45, - microsecond=631092, - ) - == datetime(2016, 7, 14, 7, 16, 45, 631092, tzinfo=tz.tzutc()) - ) - - def test_three_kwargs(self): - - assert self.factory.get(year=2016, month=7, day=14) == datetime( - 2016, 7, 14, 0, 0, tzinfo=tz.tzutc() - ) - - def test_tzinfo_string_kwargs(self): - result = self.factory.get("2019072807", "YYYYMMDDHH", tzinfo="UTC") - assert result._datetime == datetime(2019, 7, 28, 7, 0, 0, 0, tzinfo=tz.tzutc()) - - def test_insufficient_kwargs(self): - - with pytest.raises(TypeError): - self.factory.get(year=2016) - - with pytest.raises(TypeError): - self.factory.get(year=2016, month=7) - - def test_locale(self): - result = self.factory.get("2010", "YYYY", locale="ja") - assert result._datetime == datetime(2010, 1, 1, 0, 0, 0, 0, tzinfo=tz.tzutc()) - - # regression test for issue #701 - result = self.factory.get( - "Montag, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY", locale="de" - ) - assert result._datetime == datetime(2019, 9, 9, 0, 0, 0, 0, tzinfo=tz.tzutc()) - - def test_locale_kwarg_only(self): - res = self.factory.get(locale="ja") - assert res.tzinfo == tz.tzutc() - - def test_locale_with_tzinfo(self): - res = self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) - assert res.tzinfo == tz.gettz("Asia/Tokyo") - - -@pytest.mark.usefixtures("arrow_factory") -class TestUtcNow: - def test_utcnow(self): - - assert_datetime_equality( - self.factory.utcnow()._datetime, - datetime.utcnow().replace(tzinfo=tz.tzutc()), - ) - - -@pytest.mark.usefixtures("arrow_factory") -class TestNow: - def test_no_tz(self): - - assert_datetime_equality(self.factory.now(), datetime.now(tz.tzlocal())) - - def test_tzinfo(self): - - assert_datetime_equality( - self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST")) - ) - - def test_tz_str(self): - - assert_datetime_equality(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_formatter.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/test_formatter.py deleted file mode 100644 index e97aeb5dcc..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_formatter.py +++ /dev/null @@ -1,282 +0,0 @@ -# -*- coding: utf-8 -*- -from datetime import datetime - -import pytest -import pytz -from dateutil import tz as dateutil_tz - -from arrow import ( - FORMAT_ATOM, - FORMAT_COOKIE, - FORMAT_RFC822, - FORMAT_RFC850, - FORMAT_RFC1036, - FORMAT_RFC1123, - FORMAT_RFC2822, - FORMAT_RFC3339, - FORMAT_RSS, - FORMAT_W3C, -) - -from .utils import make_full_tz_list - - -@pytest.mark.usefixtures("arrow_formatter") -class TestFormatterFormatToken: - def test_format(self): - - dt = datetime(2013, 2, 5, 12, 32, 51) - - result = self.formatter.format(dt, "MM-DD-YYYY hh:mm:ss a") - - assert result == "02-05-2013 12:32:51 pm" - - def test_year(self): - - dt = datetime(2013, 1, 1) - assert self.formatter._format_token(dt, "YYYY") == "2013" - assert self.formatter._format_token(dt, "YY") == "13" - - def test_month(self): - - dt = datetime(2013, 1, 1) - assert self.formatter._format_token(dt, "MMMM") == "January" - assert self.formatter._format_token(dt, "MMM") == "Jan" - assert self.formatter._format_token(dt, "MM") == "01" - assert self.formatter._format_token(dt, "M") == "1" - - def test_day(self): - - dt = datetime(2013, 2, 1) - assert self.formatter._format_token(dt, "DDDD") == "032" - assert self.formatter._format_token(dt, "DDD") == "32" - assert self.formatter._format_token(dt, "DD") == "01" - assert self.formatter._format_token(dt, "D") == "1" - assert self.formatter._format_token(dt, "Do") == "1st" - - assert self.formatter._format_token(dt, "dddd") == "Friday" - assert self.formatter._format_token(dt, "ddd") == "Fri" - assert self.formatter._format_token(dt, "d") == "5" - - def test_hour(self): - - dt = datetime(2013, 1, 1, 2) - assert self.formatter._format_token(dt, "HH") == "02" - assert self.formatter._format_token(dt, "H") == "2" - - dt = datetime(2013, 1, 1, 13) - assert self.formatter._format_token(dt, "HH") == "13" - assert self.formatter._format_token(dt, "H") == "13" - - dt = datetime(2013, 1, 1, 2) - assert self.formatter._format_token(dt, "hh") == "02" - assert self.formatter._format_token(dt, "h") == "2" - - dt = datetime(2013, 1, 1, 13) - assert self.formatter._format_token(dt, "hh") == "01" - assert self.formatter._format_token(dt, "h") == "1" - - # test that 12-hour time converts to '12' at midnight - dt = datetime(2013, 1, 1, 0) - assert self.formatter._format_token(dt, "hh") == "12" - assert self.formatter._format_token(dt, "h") == "12" - - def test_minute(self): - - dt = datetime(2013, 1, 1, 0, 1) - assert self.formatter._format_token(dt, "mm") == "01" - assert self.formatter._format_token(dt, "m") == "1" - - def test_second(self): - - dt = datetime(2013, 1, 1, 0, 0, 1) - assert self.formatter._format_token(dt, "ss") == "01" - assert self.formatter._format_token(dt, "s") == "1" - - def test_sub_second(self): - - dt = datetime(2013, 1, 1, 0, 0, 0, 123456) - assert self.formatter._format_token(dt, "SSSSSS") == "123456" - assert self.formatter._format_token(dt, "SSSSS") == "12345" - assert self.formatter._format_token(dt, "SSSS") == "1234" - assert self.formatter._format_token(dt, "SSS") == "123" - assert self.formatter._format_token(dt, "SS") == "12" - assert self.formatter._format_token(dt, "S") == "1" - - dt = datetime(2013, 1, 1, 0, 0, 0, 2000) - assert self.formatter._format_token(dt, "SSSSSS") == "002000" - assert self.formatter._format_token(dt, "SSSSS") == "00200" - assert self.formatter._format_token(dt, "SSSS") == "0020" - assert self.formatter._format_token(dt, "SSS") == "002" - assert self.formatter._format_token(dt, "SS") == "00" - assert self.formatter._format_token(dt, "S") == "0" - - def test_timestamp(self): - - timestamp = 1588437009.8952794 - dt = datetime.utcfromtimestamp(timestamp) - expected = str(int(timestamp)) - assert self.formatter._format_token(dt, "X") == expected - - # Must round because time.time() may return a float with greater - # than 6 digits of precision - expected = str(int(timestamp * 1000000)) - assert self.formatter._format_token(dt, "x") == expected - - def test_timezone(self): - - dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz("US/Pacific")) - - result = self.formatter._format_token(dt, "ZZ") - assert result == "-07:00" or result == "-08:00" - - result = self.formatter._format_token(dt, "Z") - assert result == "-0700" or result == "-0800" - - @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) - def test_timezone_formatter(self, full_tz_name): - - # This test will fail if we use "now" as date as soon as we change from/to DST - dt = datetime(1986, 2, 14, tzinfo=pytz.timezone("UTC")).replace( - tzinfo=dateutil_tz.gettz(full_tz_name) - ) - abbreviation = dt.tzname() - - result = self.formatter._format_token(dt, "ZZZ") - assert result == abbreviation - - def test_am_pm(self): - - dt = datetime(2012, 1, 1, 11) - assert self.formatter._format_token(dt, "a") == "am" - assert self.formatter._format_token(dt, "A") == "AM" - - dt = datetime(2012, 1, 1, 13) - assert self.formatter._format_token(dt, "a") == "pm" - assert self.formatter._format_token(dt, "A") == "PM" - - def test_week(self): - dt = datetime(2017, 5, 19) - assert self.formatter._format_token(dt, "W") == "2017-W20-5" - - # make sure week is zero padded when needed - dt_early = datetime(2011, 1, 20) - assert self.formatter._format_token(dt_early, "W") == "2011-W03-4" - - def test_nonsense(self): - dt = datetime(2012, 1, 1, 11) - assert self.formatter._format_token(dt, None) is None - assert self.formatter._format_token(dt, "NONSENSE") is None - - def test_escape(self): - - assert ( - self.formatter.format( - datetime(2015, 12, 10, 17, 9), "MMMM D, YYYY [at] h:mma" - ) - == "December 10, 2015 at 5:09pm" - ) - - assert ( - self.formatter.format( - datetime(2015, 12, 10, 17, 9), "[MMMM] M D, YYYY [at] h:mma" - ) - == "MMMM 12 10, 2015 at 5:09pm" - ) - - assert ( - self.formatter.format( - datetime(1990, 11, 25), - "[It happened on] MMMM Do [in the year] YYYY [a long time ago]", - ) - == "It happened on November 25th in the year 1990 a long time ago" - ) - - assert ( - self.formatter.format( - datetime(1990, 11, 25), - "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]", - ) - == "It happened on November 25th in the year 1990 a long time ago" - ) - - assert ( - self.formatter.format( - datetime(1, 1, 1), "[I'm][ entirely][ escaped,][ weee!]" - ) - == "I'm entirely escaped, weee!" - ) - - # Special RegEx characters - assert ( - self.formatter.format( - datetime(2017, 12, 31, 2, 0), "MMM DD, YYYY |^${}().*+?<>-& h:mm A" - ) - == "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM" - ) - - # Escaping is atomic: brackets inside brackets are treated literally - assert self.formatter.format(datetime(1, 1, 1), "[[[ ]]") == "[[ ]" - - -@pytest.mark.usefixtures("arrow_formatter", "time_1975_12_25") -class TestFormatterBuiltinFormats: - def test_atom(self): - assert ( - self.formatter.format(self.datetime, FORMAT_ATOM) - == "1975-12-25 14:15:16-05:00" - ) - - def test_cookie(self): - assert ( - self.formatter.format(self.datetime, FORMAT_COOKIE) - == "Thursday, 25-Dec-1975 14:15:16 EST" - ) - - def test_rfc_822(self): - assert ( - self.formatter.format(self.datetime, FORMAT_RFC822) - == "Thu, 25 Dec 75 14:15:16 -0500" - ) - - def test_rfc_850(self): - assert ( - self.formatter.format(self.datetime, FORMAT_RFC850) - == "Thursday, 25-Dec-75 14:15:16 EST" - ) - - def test_rfc_1036(self): - assert ( - self.formatter.format(self.datetime, FORMAT_RFC1036) - == "Thu, 25 Dec 75 14:15:16 -0500" - ) - - def test_rfc_1123(self): - assert ( - self.formatter.format(self.datetime, FORMAT_RFC1123) - == "Thu, 25 Dec 1975 14:15:16 -0500" - ) - - def test_rfc_2822(self): - assert ( - self.formatter.format(self.datetime, FORMAT_RFC2822) - == "Thu, 25 Dec 1975 14:15:16 -0500" - ) - - def test_rfc3339(self): - assert ( - self.formatter.format(self.datetime, FORMAT_RFC3339) - == "1975-12-25 14:15:16-05:00" - ) - - def test_rss(self): - assert ( - self.formatter.format(self.datetime, FORMAT_RSS) - == "Thu, 25 Dec 1975 14:15:16 -0500" - ) - - def test_w3c(self): - assert ( - self.formatter.format(self.datetime, FORMAT_W3C) - == "1975-12-25 14:15:16-05:00" - ) diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_locales.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/test_locales.py deleted file mode 100644 index 006ccdd5ba..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_locales.py +++ /dev/null @@ -1,1352 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import pytest - -from arrow import arrow, locales - - -@pytest.mark.usefixtures("lang_locales") -class TestLocaleValidation: - """Validate locales to ensure that translations are valid and complete""" - - def test_locale_validation(self): - - for _, locale_cls in self.locales.items(): - # 7 days + 1 spacer to allow for 1-indexing of months - assert len(locale_cls.day_names) == 8 - assert locale_cls.day_names[0] == "" - # ensure that all string from index 1 onward are valid (not blank or None) - assert all(locale_cls.day_names[1:]) - - assert len(locale_cls.day_abbreviations) == 8 - assert locale_cls.day_abbreviations[0] == "" - assert all(locale_cls.day_abbreviations[1:]) - - # 12 months + 1 spacer to allow for 1-indexing of months - assert len(locale_cls.month_names) == 13 - assert locale_cls.month_names[0] == "" - assert all(locale_cls.month_names[1:]) - - assert len(locale_cls.month_abbreviations) == 13 - assert locale_cls.month_abbreviations[0] == "" - assert all(locale_cls.month_abbreviations[1:]) - - assert len(locale_cls.names) > 0 - assert locale_cls.past is not None - assert locale_cls.future is not None - - -class TestModule: - def test_get_locale(self, mocker): - mock_locale = mocker.Mock() - mock_locale_cls = mocker.Mock() - mock_locale_cls.return_value = mock_locale - - with pytest.raises(ValueError): - arrow.locales.get_locale("locale_name") - - cls_dict = arrow.locales._locales - mocker.patch.dict(cls_dict, {"locale_name": mock_locale_cls}) - - result = arrow.locales.get_locale("locale_name") - - assert result == mock_locale - - def test_get_locale_by_class_name(self, mocker): - mock_locale_cls = mocker.Mock() - mock_locale_obj = mock_locale_cls.return_value = mocker.Mock() - - globals_fn = mocker.Mock() - globals_fn.return_value = {"NonExistentLocale": mock_locale_cls} - - with pytest.raises(ValueError): - arrow.locales.get_locale_by_class_name("NonExistentLocale") - - mocker.patch.object(locales, "globals", globals_fn) - result = arrow.locales.get_locale_by_class_name("NonExistentLocale") - - mock_locale_cls.assert_called_once_with() - assert result == mock_locale_obj - - def test_locales(self): - - assert len(locales._locales) > 0 - - -@pytest.mark.usefixtures("lang_locale") -class TestEnglishLocale: - def test_describe(self): - assert self.locale.describe("now", only_distance=True) == "instantly" - assert self.locale.describe("now", only_distance=False) == "just now" - - def test_format_timeframe(self): - - assert self.locale._format_timeframe("hours", 2) == "2 hours" - assert self.locale._format_timeframe("hour", 0) == "an hour" - - def test_format_relative_now(self): - - result = self.locale._format_relative("just now", "now", 0) - - assert result == "just now" - - def test_format_relative_past(self): - - result = self.locale._format_relative("an hour", "hour", 1) - - assert result == "in an hour" - - def test_format_relative_future(self): - - result = self.locale._format_relative("an hour", "hour", -1) - - assert result == "an hour ago" - - def test_ordinal_number(self): - assert self.locale.ordinal_number(0) == "0th" - assert self.locale.ordinal_number(1) == "1st" - assert self.locale.ordinal_number(2) == "2nd" - assert self.locale.ordinal_number(3) == "3rd" - assert self.locale.ordinal_number(4) == "4th" - assert self.locale.ordinal_number(10) == "10th" - assert self.locale.ordinal_number(11) == "11th" - assert self.locale.ordinal_number(12) == "12th" - assert self.locale.ordinal_number(13) == "13th" - assert self.locale.ordinal_number(14) == "14th" - assert self.locale.ordinal_number(21) == "21st" - assert self.locale.ordinal_number(22) == "22nd" - assert self.locale.ordinal_number(23) == "23rd" - assert self.locale.ordinal_number(24) == "24th" - - assert self.locale.ordinal_number(100) == "100th" - assert self.locale.ordinal_number(101) == "101st" - assert self.locale.ordinal_number(102) == "102nd" - assert self.locale.ordinal_number(103) == "103rd" - assert self.locale.ordinal_number(104) == "104th" - assert self.locale.ordinal_number(110) == "110th" - assert self.locale.ordinal_number(111) == "111th" - assert self.locale.ordinal_number(112) == "112th" - assert self.locale.ordinal_number(113) == "113th" - assert self.locale.ordinal_number(114) == "114th" - assert self.locale.ordinal_number(121) == "121st" - assert self.locale.ordinal_number(122) == "122nd" - assert self.locale.ordinal_number(123) == "123rd" - assert self.locale.ordinal_number(124) == "124th" - - def test_meridian_invalid_token(self): - assert self.locale.meridian(7, None) is None - assert self.locale.meridian(7, "B") is None - assert self.locale.meridian(7, "NONSENSE") is None - - -@pytest.mark.usefixtures("lang_locale") -class TestItalianLocale: - def test_ordinal_number(self): - assert self.locale.ordinal_number(1) == "1º" - - -@pytest.mark.usefixtures("lang_locale") -class TestSpanishLocale: - def test_ordinal_number(self): - assert self.locale.ordinal_number(1) == "1º" - - def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "ahora" - assert self.locale._format_timeframe("seconds", 1) == "1 segundos" - assert self.locale._format_timeframe("seconds", 3) == "3 segundos" - assert self.locale._format_timeframe("seconds", 30) == "30 segundos" - assert self.locale._format_timeframe("minute", 1) == "un minuto" - assert self.locale._format_timeframe("minutes", 4) == "4 minutos" - assert self.locale._format_timeframe("minutes", 40) == "40 minutos" - assert self.locale._format_timeframe("hour", 1) == "una hora" - assert self.locale._format_timeframe("hours", 5) == "5 horas" - assert self.locale._format_timeframe("hours", 23) == "23 horas" - assert self.locale._format_timeframe("day", 1) == "un día" - assert self.locale._format_timeframe("days", 6) == "6 días" - assert self.locale._format_timeframe("days", 12) == "12 días" - assert self.locale._format_timeframe("week", 1) == "una semana" - assert self.locale._format_timeframe("weeks", 2) == "2 semanas" - assert self.locale._format_timeframe("weeks", 3) == "3 semanas" - assert self.locale._format_timeframe("month", 1) == "un mes" - assert self.locale._format_timeframe("months", 7) == "7 meses" - assert self.locale._format_timeframe("months", 11) == "11 meses" - assert self.locale._format_timeframe("year", 1) == "un año" - assert self.locale._format_timeframe("years", 8) == "8 años" - assert self.locale._format_timeframe("years", 12) == "12 años" - - assert self.locale._format_timeframe("now", 0) == "ahora" - assert self.locale._format_timeframe("seconds", -1) == "1 segundos" - assert self.locale._format_timeframe("seconds", -9) == "9 segundos" - assert self.locale._format_timeframe("seconds", -12) == "12 segundos" - assert self.locale._format_timeframe("minute", -1) == "un minuto" - assert self.locale._format_timeframe("minutes", -2) == "2 minutos" - assert self.locale._format_timeframe("minutes", -10) == "10 minutos" - assert self.locale._format_timeframe("hour", -1) == "una hora" - assert self.locale._format_timeframe("hours", -3) == "3 horas" - assert self.locale._format_timeframe("hours", -11) == "11 horas" - assert self.locale._format_timeframe("day", -1) == "un día" - assert self.locale._format_timeframe("days", -2) == "2 días" - assert self.locale._format_timeframe("days", -12) == "12 días" - assert self.locale._format_timeframe("week", -1) == "una semana" - assert self.locale._format_timeframe("weeks", -2) == "2 semanas" - assert self.locale._format_timeframe("weeks", -3) == "3 semanas" - assert self.locale._format_timeframe("month", -1) == "un mes" - assert self.locale._format_timeframe("months", -3) == "3 meses" - assert self.locale._format_timeframe("months", -13) == "13 meses" - assert self.locale._format_timeframe("year", -1) == "un año" - assert self.locale._format_timeframe("years", -4) == "4 años" - assert self.locale._format_timeframe("years", -14) == "14 años" - - -@pytest.mark.usefixtures("lang_locale") -class TestFrenchLocale: - def test_ordinal_number(self): - assert self.locale.ordinal_number(1) == "1er" - assert self.locale.ordinal_number(2) == "2e" - - def test_month_abbreviation(self): - assert "juil" in self.locale.month_abbreviations - - -@pytest.mark.usefixtures("lang_locale") -class TestFrenchCanadianLocale: - def test_month_abbreviation(self): - assert "juill" in self.locale.month_abbreviations - - -@pytest.mark.usefixtures("lang_locale") -class TestRussianLocale: - def test_plurals2(self): - assert self.locale._format_timeframe("hours", 0) == "0 часов" - assert self.locale._format_timeframe("hours", 1) == "1 час" - assert self.locale._format_timeframe("hours", 2) == "2 часа" - assert self.locale._format_timeframe("hours", 4) == "4 часа" - assert self.locale._format_timeframe("hours", 5) == "5 часов" - assert self.locale._format_timeframe("hours", 21) == "21 час" - assert self.locale._format_timeframe("hours", 22) == "22 часа" - assert self.locale._format_timeframe("hours", 25) == "25 часов" - - # feminine grammatical gender should be tested separately - assert self.locale._format_timeframe("minutes", 0) == "0 минут" - assert self.locale._format_timeframe("minutes", 1) == "1 минуту" - assert self.locale._format_timeframe("minutes", 2) == "2 минуты" - assert self.locale._format_timeframe("minutes", 4) == "4 минуты" - assert self.locale._format_timeframe("minutes", 5) == "5 минут" - assert self.locale._format_timeframe("minutes", 21) == "21 минуту" - assert self.locale._format_timeframe("minutes", 22) == "22 минуты" - assert self.locale._format_timeframe("minutes", 25) == "25 минут" - - -@pytest.mark.usefixtures("lang_locale") -class TestPolishLocale: - def test_plurals(self): - - assert self.locale._format_timeframe("seconds", 0) == "0 sekund" - assert self.locale._format_timeframe("second", 1) == "sekundę" - assert self.locale._format_timeframe("seconds", 2) == "2 sekundy" - assert self.locale._format_timeframe("seconds", 5) == "5 sekund" - assert self.locale._format_timeframe("seconds", 21) == "21 sekund" - assert self.locale._format_timeframe("seconds", 22) == "22 sekundy" - assert self.locale._format_timeframe("seconds", 25) == "25 sekund" - - assert self.locale._format_timeframe("minutes", 0) == "0 minut" - assert self.locale._format_timeframe("minute", 1) == "minutę" - assert self.locale._format_timeframe("minutes", 2) == "2 minuty" - assert self.locale._format_timeframe("minutes", 5) == "5 minut" - assert self.locale._format_timeframe("minutes", 21) == "21 minut" - assert self.locale._format_timeframe("minutes", 22) == "22 minuty" - assert self.locale._format_timeframe("minutes", 25) == "25 minut" - - assert self.locale._format_timeframe("hours", 0) == "0 godzin" - assert self.locale._format_timeframe("hour", 1) == "godzinę" - assert self.locale._format_timeframe("hours", 2) == "2 godziny" - assert self.locale._format_timeframe("hours", 5) == "5 godzin" - assert self.locale._format_timeframe("hours", 21) == "21 godzin" - assert self.locale._format_timeframe("hours", 22) == "22 godziny" - assert self.locale._format_timeframe("hours", 25) == "25 godzin" - - assert self.locale._format_timeframe("weeks", 0) == "0 tygodni" - assert self.locale._format_timeframe("week", 1) == "tydzień" - assert self.locale._format_timeframe("weeks", 2) == "2 tygodnie" - assert self.locale._format_timeframe("weeks", 5) == "5 tygodni" - assert self.locale._format_timeframe("weeks", 21) == "21 tygodni" - assert self.locale._format_timeframe("weeks", 22) == "22 tygodnie" - assert self.locale._format_timeframe("weeks", 25) == "25 tygodni" - - assert self.locale._format_timeframe("months", 0) == "0 miesięcy" - assert self.locale._format_timeframe("month", 1) == "miesiąc" - assert self.locale._format_timeframe("months", 2) == "2 miesiące" - assert self.locale._format_timeframe("months", 5) == "5 miesięcy" - assert self.locale._format_timeframe("months", 21) == "21 miesięcy" - assert self.locale._format_timeframe("months", 22) == "22 miesiące" - assert self.locale._format_timeframe("months", 25) == "25 miesięcy" - - assert self.locale._format_timeframe("years", 0) == "0 lat" - assert self.locale._format_timeframe("year", 1) == "rok" - assert self.locale._format_timeframe("years", 2) == "2 lata" - assert self.locale._format_timeframe("years", 5) == "5 lat" - assert self.locale._format_timeframe("years", 21) == "21 lat" - assert self.locale._format_timeframe("years", 22) == "22 lata" - assert self.locale._format_timeframe("years", 25) == "25 lat" - - -@pytest.mark.usefixtures("lang_locale") -class TestIcelandicLocale: - def test_format_timeframe(self): - - assert self.locale._format_timeframe("minute", -1) == "einni mínútu" - assert self.locale._format_timeframe("minute", 1) == "eina mínútu" - - assert self.locale._format_timeframe("hours", -2) == "2 tímum" - assert self.locale._format_timeframe("hours", 2) == "2 tíma" - assert self.locale._format_timeframe("now", 0) == "rétt í þessu" - - -@pytest.mark.usefixtures("lang_locale") -class TestMalayalamLocale: - def test_format_timeframe(self): - - assert self.locale._format_timeframe("hours", 2) == "2 മണിക്കൂർ" - assert self.locale._format_timeframe("hour", 0) == "ഒരു മണിക്കൂർ" - - def test_format_relative_now(self): - - result = self.locale._format_relative("ഇപ്പോൾ", "now", 0) - - assert result == "ഇപ്പോൾ" - - def test_format_relative_past(self): - - result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", 1) - assert result == "ഒരു മണിക്കൂർ ശേഷം" - - def test_format_relative_future(self): - - result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", -1) - assert result == "ഒരു മണിക്കൂർ മുമ്പ്" - - -@pytest.mark.usefixtures("lang_locale") -class TestHindiLocale: - def test_format_timeframe(self): - - assert self.locale._format_timeframe("hours", 2) == "2 घंटे" - assert self.locale._format_timeframe("hour", 0) == "एक घंटा" - - def test_format_relative_now(self): - - result = self.locale._format_relative("अभी", "now", 0) - assert result == "अभी" - - def test_format_relative_past(self): - - result = self.locale._format_relative("एक घंटा", "hour", 1) - assert result == "एक घंटा बाद" - - def test_format_relative_future(self): - - result = self.locale._format_relative("एक घंटा", "hour", -1) - assert result == "एक घंटा पहले" - - -@pytest.mark.usefixtures("lang_locale") -class TestCzechLocale: - def test_format_timeframe(self): - - assert self.locale._format_timeframe("hours", 2) == "2 hodiny" - assert self.locale._format_timeframe("hours", 5) == "5 hodin" - assert self.locale._format_timeframe("hour", 0) == "0 hodin" - assert self.locale._format_timeframe("hours", -2) == "2 hodinami" - assert self.locale._format_timeframe("hours", -5) == "5 hodinami" - assert self.locale._format_timeframe("now", 0) == "Teď" - - assert self.locale._format_timeframe("weeks", 2) == "2 týdny" - assert self.locale._format_timeframe("weeks", 5) == "5 týdnů" - assert self.locale._format_timeframe("week", 0) == "0 týdnů" - assert self.locale._format_timeframe("weeks", -2) == "2 týdny" - assert self.locale._format_timeframe("weeks", -5) == "5 týdny" - - def test_format_relative_now(self): - - result = self.locale._format_relative("Teď", "now", 0) - assert result == "Teď" - - def test_format_relative_future(self): - - result = self.locale._format_relative("hodinu", "hour", 1) - assert result == "Za hodinu" - - def test_format_relative_past(self): - - result = self.locale._format_relative("hodinou", "hour", -1) - assert result == "Před hodinou" - - -@pytest.mark.usefixtures("lang_locale") -class TestSlovakLocale: - def test_format_timeframe(self): - - assert self.locale._format_timeframe("seconds", -5) == "5 sekundami" - assert self.locale._format_timeframe("seconds", -2) == "2 sekundami" - assert self.locale._format_timeframe("second", -1) == "sekundou" - assert self.locale._format_timeframe("second", 0) == "0 sekúnd" - assert self.locale._format_timeframe("second", 1) == "sekundu" - assert self.locale._format_timeframe("seconds", 2) == "2 sekundy" - assert self.locale._format_timeframe("seconds", 5) == "5 sekúnd" - - assert self.locale._format_timeframe("minutes", -5) == "5 minútami" - assert self.locale._format_timeframe("minutes", -2) == "2 minútami" - assert self.locale._format_timeframe("minute", -1) == "minútou" - assert self.locale._format_timeframe("minute", 0) == "0 minút" - assert self.locale._format_timeframe("minute", 1) == "minútu" - assert self.locale._format_timeframe("minutes", 2) == "2 minúty" - assert self.locale._format_timeframe("minutes", 5) == "5 minút" - - assert self.locale._format_timeframe("hours", -5) == "5 hodinami" - assert self.locale._format_timeframe("hours", -2) == "2 hodinami" - assert self.locale._format_timeframe("hour", -1) == "hodinou" - assert self.locale._format_timeframe("hour", 0) == "0 hodín" - assert self.locale._format_timeframe("hour", 1) == "hodinu" - assert self.locale._format_timeframe("hours", 2) == "2 hodiny" - assert self.locale._format_timeframe("hours", 5) == "5 hodín" - - assert self.locale._format_timeframe("days", -5) == "5 dňami" - assert self.locale._format_timeframe("days", -2) == "2 dňami" - assert self.locale._format_timeframe("day", -1) == "dňom" - assert self.locale._format_timeframe("day", 0) == "0 dní" - assert self.locale._format_timeframe("day", 1) == "deň" - assert self.locale._format_timeframe("days", 2) == "2 dni" - assert self.locale._format_timeframe("days", 5) == "5 dní" - - assert self.locale._format_timeframe("weeks", -5) == "5 týždňami" - assert self.locale._format_timeframe("weeks", -2) == "2 týždňami" - assert self.locale._format_timeframe("week", -1) == "týždňom" - assert self.locale._format_timeframe("week", 0) == "0 týždňov" - assert self.locale._format_timeframe("week", 1) == "týždeň" - assert self.locale._format_timeframe("weeks", 2) == "2 týždne" - assert self.locale._format_timeframe("weeks", 5) == "5 týždňov" - - assert self.locale._format_timeframe("months", -5) == "5 mesiacmi" - assert self.locale._format_timeframe("months", -2) == "2 mesiacmi" - assert self.locale._format_timeframe("month", -1) == "mesiacom" - assert self.locale._format_timeframe("month", 0) == "0 mesiacov" - assert self.locale._format_timeframe("month", 1) == "mesiac" - assert self.locale._format_timeframe("months", 2) == "2 mesiace" - assert self.locale._format_timeframe("months", 5) == "5 mesiacov" - - assert self.locale._format_timeframe("years", -5) == "5 rokmi" - assert self.locale._format_timeframe("years", -2) == "2 rokmi" - assert self.locale._format_timeframe("year", -1) == "rokom" - assert self.locale._format_timeframe("year", 0) == "0 rokov" - assert self.locale._format_timeframe("year", 1) == "rok" - assert self.locale._format_timeframe("years", 2) == "2 roky" - assert self.locale._format_timeframe("years", 5) == "5 rokov" - - assert self.locale._format_timeframe("now", 0) == "Teraz" - - def test_format_relative_now(self): - - result = self.locale._format_relative("Teraz", "now", 0) - assert result == "Teraz" - - def test_format_relative_future(self): - - result = self.locale._format_relative("hodinu", "hour", 1) - assert result == "O hodinu" - - def test_format_relative_past(self): - - result = self.locale._format_relative("hodinou", "hour", -1) - assert result == "Pred hodinou" - - -@pytest.mark.usefixtures("lang_locale") -class TestBulgarianLocale: - def test_plurals2(self): - assert self.locale._format_timeframe("hours", 0) == "0 часа" - assert self.locale._format_timeframe("hours", 1) == "1 час" - assert self.locale._format_timeframe("hours", 2) == "2 часа" - assert self.locale._format_timeframe("hours", 4) == "4 часа" - assert self.locale._format_timeframe("hours", 5) == "5 часа" - assert self.locale._format_timeframe("hours", 21) == "21 час" - assert self.locale._format_timeframe("hours", 22) == "22 часа" - assert self.locale._format_timeframe("hours", 25) == "25 часа" - - # feminine grammatical gender should be tested separately - assert self.locale._format_timeframe("minutes", 0) == "0 минути" - assert self.locale._format_timeframe("minutes", 1) == "1 минута" - assert self.locale._format_timeframe("minutes", 2) == "2 минути" - assert self.locale._format_timeframe("minutes", 4) == "4 минути" - assert self.locale._format_timeframe("minutes", 5) == "5 минути" - assert self.locale._format_timeframe("minutes", 21) == "21 минута" - assert self.locale._format_timeframe("minutes", 22) == "22 минути" - assert self.locale._format_timeframe("minutes", 25) == "25 минути" - - -@pytest.mark.usefixtures("lang_locale") -class TestMacedonianLocale: - def test_singles_mk(self): - assert self.locale._format_timeframe("second", 1) == "една секунда" - assert self.locale._format_timeframe("minute", 1) == "една минута" - assert self.locale._format_timeframe("hour", 1) == "еден саат" - assert self.locale._format_timeframe("day", 1) == "еден ден" - assert self.locale._format_timeframe("week", 1) == "една недела" - assert self.locale._format_timeframe("month", 1) == "еден месец" - assert self.locale._format_timeframe("year", 1) == "една година" - - def test_meridians_mk(self): - assert self.locale.meridian(7, "A") == "претпладне" - assert self.locale.meridian(18, "A") == "попладне" - assert self.locale.meridian(10, "a") == "дп" - assert self.locale.meridian(22, "a") == "пп" - - def test_describe_mk(self): - assert self.locale.describe("second", only_distance=True) == "една секунда" - assert self.locale.describe("second", only_distance=False) == "за една секунда" - assert self.locale.describe("minute", only_distance=True) == "една минута" - assert self.locale.describe("minute", only_distance=False) == "за една минута" - assert self.locale.describe("hour", only_distance=True) == "еден саат" - assert self.locale.describe("hour", only_distance=False) == "за еден саат" - assert self.locale.describe("day", only_distance=True) == "еден ден" - assert self.locale.describe("day", only_distance=False) == "за еден ден" - assert self.locale.describe("week", only_distance=True) == "една недела" - assert self.locale.describe("week", only_distance=False) == "за една недела" - assert self.locale.describe("month", only_distance=True) == "еден месец" - assert self.locale.describe("month", only_distance=False) == "за еден месец" - assert self.locale.describe("year", only_distance=True) == "една година" - assert self.locale.describe("year", only_distance=False) == "за една година" - - def test_relative_mk(self): - # time - assert self.locale._format_relative("сега", "now", 0) == "сега" - assert self.locale._format_relative("1 секунда", "seconds", 1) == "за 1 секунда" - assert self.locale._format_relative("1 минута", "minutes", 1) == "за 1 минута" - assert self.locale._format_relative("1 саат", "hours", 1) == "за 1 саат" - assert self.locale._format_relative("1 ден", "days", 1) == "за 1 ден" - assert self.locale._format_relative("1 недела", "weeks", 1) == "за 1 недела" - assert self.locale._format_relative("1 месец", "months", 1) == "за 1 месец" - assert self.locale._format_relative("1 година", "years", 1) == "за 1 година" - assert ( - self.locale._format_relative("1 секунда", "seconds", -1) == "пред 1 секунда" - ) - assert ( - self.locale._format_relative("1 минута", "minutes", -1) == "пред 1 минута" - ) - assert self.locale._format_relative("1 саат", "hours", -1) == "пред 1 саат" - assert self.locale._format_relative("1 ден", "days", -1) == "пред 1 ден" - assert self.locale._format_relative("1 недела", "weeks", -1) == "пред 1 недела" - assert self.locale._format_relative("1 месец", "months", -1) == "пред 1 месец" - assert self.locale._format_relative("1 година", "years", -1) == "пред 1 година" - - def test_plurals_mk(self): - # Seconds - assert self.locale._format_timeframe("seconds", 0) == "0 секунди" - assert self.locale._format_timeframe("seconds", 1) == "1 секунда" - assert self.locale._format_timeframe("seconds", 2) == "2 секунди" - assert self.locale._format_timeframe("seconds", 4) == "4 секунди" - assert self.locale._format_timeframe("seconds", 5) == "5 секунди" - assert self.locale._format_timeframe("seconds", 21) == "21 секунда" - assert self.locale._format_timeframe("seconds", 22) == "22 секунди" - assert self.locale._format_timeframe("seconds", 25) == "25 секунди" - - # Minutes - assert self.locale._format_timeframe("minutes", 0) == "0 минути" - assert self.locale._format_timeframe("minutes", 1) == "1 минута" - assert self.locale._format_timeframe("minutes", 2) == "2 минути" - assert self.locale._format_timeframe("minutes", 4) == "4 минути" - assert self.locale._format_timeframe("minutes", 5) == "5 минути" - assert self.locale._format_timeframe("minutes", 21) == "21 минута" - assert self.locale._format_timeframe("minutes", 22) == "22 минути" - assert self.locale._format_timeframe("minutes", 25) == "25 минути" - - # Hours - assert self.locale._format_timeframe("hours", 0) == "0 саати" - assert self.locale._format_timeframe("hours", 1) == "1 саат" - assert self.locale._format_timeframe("hours", 2) == "2 саати" - assert self.locale._format_timeframe("hours", 4) == "4 саати" - assert self.locale._format_timeframe("hours", 5) == "5 саати" - assert self.locale._format_timeframe("hours", 21) == "21 саат" - assert self.locale._format_timeframe("hours", 22) == "22 саати" - assert self.locale._format_timeframe("hours", 25) == "25 саати" - - # Days - assert self.locale._format_timeframe("days", 0) == "0 дена" - assert self.locale._format_timeframe("days", 1) == "1 ден" - assert self.locale._format_timeframe("days", 2) == "2 дена" - assert self.locale._format_timeframe("days", 3) == "3 дена" - assert self.locale._format_timeframe("days", 21) == "21 ден" - - # Weeks - assert self.locale._format_timeframe("weeks", 0) == "0 недели" - assert self.locale._format_timeframe("weeks", 1) == "1 недела" - assert self.locale._format_timeframe("weeks", 2) == "2 недели" - assert self.locale._format_timeframe("weeks", 4) == "4 недели" - assert self.locale._format_timeframe("weeks", 5) == "5 недели" - assert self.locale._format_timeframe("weeks", 21) == "21 недела" - assert self.locale._format_timeframe("weeks", 22) == "22 недели" - assert self.locale._format_timeframe("weeks", 25) == "25 недели" - - # Months - assert self.locale._format_timeframe("months", 0) == "0 месеци" - assert self.locale._format_timeframe("months", 1) == "1 месец" - assert self.locale._format_timeframe("months", 2) == "2 месеци" - assert self.locale._format_timeframe("months", 4) == "4 месеци" - assert self.locale._format_timeframe("months", 5) == "5 месеци" - assert self.locale._format_timeframe("months", 21) == "21 месец" - assert self.locale._format_timeframe("months", 22) == "22 месеци" - assert self.locale._format_timeframe("months", 25) == "25 месеци" - - # Years - assert self.locale._format_timeframe("years", 1) == "1 година" - assert self.locale._format_timeframe("years", 2) == "2 години" - assert self.locale._format_timeframe("years", 5) == "5 години" - - def test_multi_describe_mk(self): - describe = self.locale.describe_multi - - fulltest = [("years", 5), ("weeks", 1), ("hours", 1), ("minutes", 6)] - assert describe(fulltest) == "за 5 години 1 недела 1 саат 6 минути" - seconds4000_0days = [("days", 0), ("hours", 1), ("minutes", 6)] - assert describe(seconds4000_0days) == "за 0 дена 1 саат 6 минути" - seconds4000 = [("hours", 1), ("minutes", 6)] - assert describe(seconds4000) == "за 1 саат 6 минути" - assert describe(seconds4000, only_distance=True) == "1 саат 6 минути" - seconds3700 = [("hours", 1), ("minutes", 1)] - assert describe(seconds3700) == "за 1 саат 1 минута" - seconds300_0hours = [("hours", 0), ("minutes", 5)] - assert describe(seconds300_0hours) == "за 0 саати 5 минути" - seconds300 = [("minutes", 5)] - assert describe(seconds300) == "за 5 минути" - seconds60 = [("minutes", 1)] - assert describe(seconds60) == "за 1 минута" - assert describe(seconds60, only_distance=True) == "1 минута" - seconds60 = [("seconds", 1)] - assert describe(seconds60) == "за 1 секунда" - assert describe(seconds60, only_distance=True) == "1 секунда" - - -@pytest.mark.usefixtures("time_2013_01_01") -@pytest.mark.usefixtures("lang_locale") -class TestHebrewLocale: - def test_couple_of_timeframe(self): - assert self.locale._format_timeframe("days", 1) == "יום" - assert self.locale._format_timeframe("days", 2) == "יומיים" - assert self.locale._format_timeframe("days", 3) == "3 ימים" - - assert self.locale._format_timeframe("hours", 1) == "שעה" - assert self.locale._format_timeframe("hours", 2) == "שעתיים" - assert self.locale._format_timeframe("hours", 3) == "3 שעות" - - assert self.locale._format_timeframe("week", 1) == "שבוע" - assert self.locale._format_timeframe("weeks", 2) == "שבועיים" - assert self.locale._format_timeframe("weeks", 3) == "3 שבועות" - - assert self.locale._format_timeframe("months", 1) == "חודש" - assert self.locale._format_timeframe("months", 2) == "חודשיים" - assert self.locale._format_timeframe("months", 4) == "4 חודשים" - - assert self.locale._format_timeframe("years", 1) == "שנה" - assert self.locale._format_timeframe("years", 2) == "שנתיים" - assert self.locale._format_timeframe("years", 5) == "5 שנים" - - def test_describe_multi(self): - describe = self.locale.describe_multi - - fulltest = [("years", 5), ("weeks", 1), ("hours", 1), ("minutes", 6)] - assert describe(fulltest) == "בעוד 5 שנים, שבוע, שעה ו־6 דקות" - seconds4000_0days = [("days", 0), ("hours", 1), ("minutes", 6)] - assert describe(seconds4000_0days) == "בעוד 0 ימים, שעה ו־6 דקות" - seconds4000 = [("hours", 1), ("minutes", 6)] - assert describe(seconds4000) == "בעוד שעה ו־6 דקות" - assert describe(seconds4000, only_distance=True) == "שעה ו־6 דקות" - seconds3700 = [("hours", 1), ("minutes", 1)] - assert describe(seconds3700) == "בעוד שעה ודקה" - seconds300_0hours = [("hours", 0), ("minutes", 5)] - assert describe(seconds300_0hours) == "בעוד 0 שעות ו־5 דקות" - seconds300 = [("minutes", 5)] - assert describe(seconds300) == "בעוד 5 דקות" - seconds60 = [("minutes", 1)] - assert describe(seconds60) == "בעוד דקה" - assert describe(seconds60, only_distance=True) == "דקה" - - -@pytest.mark.usefixtures("lang_locale") -class TestMarathiLocale: - def test_dateCoreFunctionality(self): - dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) - assert self.locale.month_name(dt.month) == "एप्रिल" - assert self.locale.month_abbreviation(dt.month) == "एप्रि" - assert self.locale.day_name(dt.isoweekday()) == "शनिवार" - assert self.locale.day_abbreviation(dt.isoweekday()) == "शनि" - - def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 तास" - assert self.locale._format_timeframe("hour", 0) == "एक तास" - - def test_format_relative_now(self): - result = self.locale._format_relative("सद्य", "now", 0) - assert result == "सद्य" - - def test_format_relative_past(self): - result = self.locale._format_relative("एक तास", "hour", 1) - assert result == "एक तास नंतर" - - def test_format_relative_future(self): - result = self.locale._format_relative("एक तास", "hour", -1) - assert result == "एक तास आधी" - - # Not currently implemented - def test_ordinal_number(self): - assert self.locale.ordinal_number(1) == "1" - - -@pytest.mark.usefixtures("lang_locale") -class TestFinnishLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == ("2 tuntia", "2 tunnin") - assert self.locale._format_timeframe("hour", 0) == ("tunti", "tunnin") - - def test_format_relative_now(self): - result = self.locale._format_relative(["juuri nyt", "juuri nyt"], "now", 0) - assert result == "juuri nyt" - - def test_format_relative_past(self): - result = self.locale._format_relative(["tunti", "tunnin"], "hour", 1) - assert result == "tunnin kuluttua" - - def test_format_relative_future(self): - result = self.locale._format_relative(["tunti", "tunnin"], "hour", -1) - assert result == "tunti sitten" - - def test_ordinal_number(self): - assert self.locale.ordinal_number(1) == "1." - - -@pytest.mark.usefixtures("lang_locale") -class TestGermanLocale: - def test_ordinal_number(self): - assert self.locale.ordinal_number(1) == "1." - - def test_define(self): - assert self.locale.describe("minute", only_distance=True) == "eine Minute" - assert self.locale.describe("minute", only_distance=False) == "in einer Minute" - assert self.locale.describe("hour", only_distance=True) == "eine Stunde" - assert self.locale.describe("hour", only_distance=False) == "in einer Stunde" - assert self.locale.describe("day", only_distance=True) == "ein Tag" - assert self.locale.describe("day", only_distance=False) == "in einem Tag" - assert self.locale.describe("week", only_distance=True) == "eine Woche" - assert self.locale.describe("week", only_distance=False) == "in einer Woche" - assert self.locale.describe("month", only_distance=True) == "ein Monat" - assert self.locale.describe("month", only_distance=False) == "in einem Monat" - assert self.locale.describe("year", only_distance=True) == "ein Jahr" - assert self.locale.describe("year", only_distance=False) == "in einem Jahr" - - def test_weekday(self): - dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) - assert self.locale.day_name(dt.isoweekday()) == "Samstag" - assert self.locale.day_abbreviation(dt.isoweekday()) == "Sa" - - -@pytest.mark.usefixtures("lang_locale") -class TestHungarianLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 óra" - assert self.locale._format_timeframe("hour", 0) == "egy órával" - assert self.locale._format_timeframe("hours", -2) == "2 órával" - assert self.locale._format_timeframe("now", 0) == "éppen most" - - -@pytest.mark.usefixtures("lang_locale") -class TestEsperantoLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 horoj" - assert self.locale._format_timeframe("hour", 0) == "un horo" - assert self.locale._format_timeframe("hours", -2) == "2 horoj" - assert self.locale._format_timeframe("now", 0) == "nun" - - def test_ordinal_number(self): - assert self.locale.ordinal_number(1) == "1a" - - -@pytest.mark.usefixtures("lang_locale") -class TestThaiLocale: - def test_year_full(self): - assert self.locale.year_full(2015) == "2558" - - def test_year_abbreviation(self): - assert self.locale.year_abbreviation(2015) == "58" - - def test_format_relative_now(self): - result = self.locale._format_relative("ขณะนี้", "now", 0) - assert result == "ขณะนี้" - - def test_format_relative_past(self): - result = self.locale._format_relative("1 ชั่วโมง", "hour", 1) - assert result == "ในอีก 1 ชั่วโมง" - result = self.locale._format_relative("{0} ชั่วโมง", "hours", 2) - assert result == "ในอีก {0} ชั่วโมง" - result = self.locale._format_relative("ไม่กี่วินาที", "seconds", 42) - assert result == "ในอีกไม่กี่วินาที" - - def test_format_relative_future(self): - result = self.locale._format_relative("1 ชั่วโมง", "hour", -1) - assert result == "1 ชั่วโมง ที่ผ่านมา" - - -@pytest.mark.usefixtures("lang_locale") -class TestBengaliLocale: - def test_ordinal_number(self): - assert self.locale._ordinal_number(0) == "0তম" - assert self.locale._ordinal_number(1) == "1ম" - assert self.locale._ordinal_number(3) == "3য়" - assert self.locale._ordinal_number(4) == "4র্থ" - assert self.locale._ordinal_number(5) == "5ম" - assert self.locale._ordinal_number(6) == "6ষ্ঠ" - assert self.locale._ordinal_number(10) == "10ম" - assert self.locale._ordinal_number(11) == "11তম" - assert self.locale._ordinal_number(42) == "42তম" - assert self.locale._ordinal_number(-1) is None - - -@pytest.mark.usefixtures("lang_locale") -class TestRomanianLocale: - def test_timeframes(self): - - assert self.locale._format_timeframe("hours", 2) == "2 ore" - assert self.locale._format_timeframe("months", 2) == "2 luni" - - assert self.locale._format_timeframe("days", 2) == "2 zile" - assert self.locale._format_timeframe("years", 2) == "2 ani" - - assert self.locale._format_timeframe("hours", 3) == "3 ore" - assert self.locale._format_timeframe("months", 4) == "4 luni" - assert self.locale._format_timeframe("days", 3) == "3 zile" - assert self.locale._format_timeframe("years", 5) == "5 ani" - - def test_relative_timeframes(self): - assert self.locale._format_relative("acum", "now", 0) == "acum" - assert self.locale._format_relative("o oră", "hour", 1) == "peste o oră" - assert self.locale._format_relative("o oră", "hour", -1) == "o oră în urmă" - assert self.locale._format_relative("un minut", "minute", 1) == "peste un minut" - assert ( - self.locale._format_relative("un minut", "minute", -1) == "un minut în urmă" - ) - assert ( - self.locale._format_relative("câteva secunde", "seconds", -1) - == "câteva secunde în urmă" - ) - assert ( - self.locale._format_relative("câteva secunde", "seconds", 1) - == "peste câteva secunde" - ) - assert self.locale._format_relative("o zi", "day", -1) == "o zi în urmă" - assert self.locale._format_relative("o zi", "day", 1) == "peste o zi" - - -@pytest.mark.usefixtures("lang_locale") -class TestArabicLocale: - def test_timeframes(self): - - # single - assert self.locale._format_timeframe("minute", 1) == "دقيقة" - assert self.locale._format_timeframe("hour", 1) == "ساعة" - assert self.locale._format_timeframe("day", 1) == "يوم" - assert self.locale._format_timeframe("month", 1) == "شهر" - assert self.locale._format_timeframe("year", 1) == "سنة" - - # double - assert self.locale._format_timeframe("minutes", 2) == "دقيقتين" - assert self.locale._format_timeframe("hours", 2) == "ساعتين" - assert self.locale._format_timeframe("days", 2) == "يومين" - assert self.locale._format_timeframe("months", 2) == "شهرين" - assert self.locale._format_timeframe("years", 2) == "سنتين" - - # up to ten - assert self.locale._format_timeframe("minutes", 3) == "3 دقائق" - assert self.locale._format_timeframe("hours", 4) == "4 ساعات" - assert self.locale._format_timeframe("days", 5) == "5 أيام" - assert self.locale._format_timeframe("months", 6) == "6 أشهر" - assert self.locale._format_timeframe("years", 10) == "10 سنوات" - - # more than ten - assert self.locale._format_timeframe("minutes", 11) == "11 دقيقة" - assert self.locale._format_timeframe("hours", 19) == "19 ساعة" - assert self.locale._format_timeframe("months", 24) == "24 شهر" - assert self.locale._format_timeframe("days", 50) == "50 يوم" - assert self.locale._format_timeframe("years", 115) == "115 سنة" - - -@pytest.mark.usefixtures("lang_locale") -class TestNepaliLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 3) == "3 घण्टा" - assert self.locale._format_timeframe("hour", 0) == "एक घण्टा" - - def test_format_relative_now(self): - result = self.locale._format_relative("अहिले", "now", 0) - assert result == "अहिले" - - def test_format_relative_future(self): - result = self.locale._format_relative("एक घण्टा", "hour", 1) - assert result == "एक घण्टा पछी" - - def test_format_relative_past(self): - result = self.locale._format_relative("एक घण्टा", "hour", -1) - assert result == "एक घण्टा पहिले" - - -@pytest.mark.usefixtures("lang_locale") -class TestIndonesianLocale: - def test_timeframes(self): - assert self.locale._format_timeframe("hours", 2) == "2 jam" - assert self.locale._format_timeframe("months", 2) == "2 bulan" - - assert self.locale._format_timeframe("days", 2) == "2 hari" - assert self.locale._format_timeframe("years", 2) == "2 tahun" - - assert self.locale._format_timeframe("hours", 3) == "3 jam" - assert self.locale._format_timeframe("months", 4) == "4 bulan" - assert self.locale._format_timeframe("days", 3) == "3 hari" - assert self.locale._format_timeframe("years", 5) == "5 tahun" - - def test_format_relative_now(self): - assert self.locale._format_relative("baru saja", "now", 0) == "baru saja" - - def test_format_relative_past(self): - assert self.locale._format_relative("1 jam", "hour", 1) == "dalam 1 jam" - assert self.locale._format_relative("1 detik", "seconds", 1) == "dalam 1 detik" - - def test_format_relative_future(self): - assert self.locale._format_relative("1 jam", "hour", -1) == "1 jam yang lalu" - - -@pytest.mark.usefixtures("lang_locale") -class TestTagalogLocale: - def test_singles_tl(self): - assert self.locale._format_timeframe("second", 1) == "isang segundo" - assert self.locale._format_timeframe("minute", 1) == "isang minuto" - assert self.locale._format_timeframe("hour", 1) == "isang oras" - assert self.locale._format_timeframe("day", 1) == "isang araw" - assert self.locale._format_timeframe("week", 1) == "isang linggo" - assert self.locale._format_timeframe("month", 1) == "isang buwan" - assert self.locale._format_timeframe("year", 1) == "isang taon" - - def test_meridians_tl(self): - assert self.locale.meridian(7, "A") == "ng umaga" - assert self.locale.meridian(18, "A") == "ng hapon" - assert self.locale.meridian(10, "a") == "nu" - assert self.locale.meridian(22, "a") == "nh" - - def test_describe_tl(self): - assert self.locale.describe("second", only_distance=True) == "isang segundo" - assert ( - self.locale.describe("second", only_distance=False) - == "isang segundo mula ngayon" - ) - assert self.locale.describe("minute", only_distance=True) == "isang minuto" - assert ( - self.locale.describe("minute", only_distance=False) - == "isang minuto mula ngayon" - ) - assert self.locale.describe("hour", only_distance=True) == "isang oras" - assert ( - self.locale.describe("hour", only_distance=False) - == "isang oras mula ngayon" - ) - assert self.locale.describe("day", only_distance=True) == "isang araw" - assert ( - self.locale.describe("day", only_distance=False) == "isang araw mula ngayon" - ) - assert self.locale.describe("week", only_distance=True) == "isang linggo" - assert ( - self.locale.describe("week", only_distance=False) - == "isang linggo mula ngayon" - ) - assert self.locale.describe("month", only_distance=True) == "isang buwan" - assert ( - self.locale.describe("month", only_distance=False) - == "isang buwan mula ngayon" - ) - assert self.locale.describe("year", only_distance=True) == "isang taon" - assert ( - self.locale.describe("year", only_distance=False) - == "isang taon mula ngayon" - ) - - def test_relative_tl(self): - # time - assert self.locale._format_relative("ngayon", "now", 0) == "ngayon" - assert ( - self.locale._format_relative("1 segundo", "seconds", 1) - == "1 segundo mula ngayon" - ) - assert ( - self.locale._format_relative("1 minuto", "minutes", 1) - == "1 minuto mula ngayon" - ) - assert ( - self.locale._format_relative("1 oras", "hours", 1) == "1 oras mula ngayon" - ) - assert self.locale._format_relative("1 araw", "days", 1) == "1 araw mula ngayon" - assert ( - self.locale._format_relative("1 linggo", "weeks", 1) - == "1 linggo mula ngayon" - ) - assert ( - self.locale._format_relative("1 buwan", "months", 1) - == "1 buwan mula ngayon" - ) - assert ( - self.locale._format_relative("1 taon", "years", 1) == "1 taon mula ngayon" - ) - assert ( - self.locale._format_relative("1 segundo", "seconds", -1) - == "nakaraang 1 segundo" - ) - assert ( - self.locale._format_relative("1 minuto", "minutes", -1) - == "nakaraang 1 minuto" - ) - assert self.locale._format_relative("1 oras", "hours", -1) == "nakaraang 1 oras" - assert self.locale._format_relative("1 araw", "days", -1) == "nakaraang 1 araw" - assert ( - self.locale._format_relative("1 linggo", "weeks", -1) - == "nakaraang 1 linggo" - ) - assert ( - self.locale._format_relative("1 buwan", "months", -1) == "nakaraang 1 buwan" - ) - assert self.locale._format_relative("1 taon", "years", -1) == "nakaraang 1 taon" - - def test_plurals_tl(self): - # Seconds - assert self.locale._format_timeframe("seconds", 0) == "0 segundo" - assert self.locale._format_timeframe("seconds", 1) == "1 segundo" - assert self.locale._format_timeframe("seconds", 2) == "2 segundo" - assert self.locale._format_timeframe("seconds", 4) == "4 segundo" - assert self.locale._format_timeframe("seconds", 5) == "5 segundo" - assert self.locale._format_timeframe("seconds", 21) == "21 segundo" - assert self.locale._format_timeframe("seconds", 22) == "22 segundo" - assert self.locale._format_timeframe("seconds", 25) == "25 segundo" - - # Minutes - assert self.locale._format_timeframe("minutes", 0) == "0 minuto" - assert self.locale._format_timeframe("minutes", 1) == "1 minuto" - assert self.locale._format_timeframe("minutes", 2) == "2 minuto" - assert self.locale._format_timeframe("minutes", 4) == "4 minuto" - assert self.locale._format_timeframe("minutes", 5) == "5 minuto" - assert self.locale._format_timeframe("minutes", 21) == "21 minuto" - assert self.locale._format_timeframe("minutes", 22) == "22 minuto" - assert self.locale._format_timeframe("minutes", 25) == "25 minuto" - - # Hours - assert self.locale._format_timeframe("hours", 0) == "0 oras" - assert self.locale._format_timeframe("hours", 1) == "1 oras" - assert self.locale._format_timeframe("hours", 2) == "2 oras" - assert self.locale._format_timeframe("hours", 4) == "4 oras" - assert self.locale._format_timeframe("hours", 5) == "5 oras" - assert self.locale._format_timeframe("hours", 21) == "21 oras" - assert self.locale._format_timeframe("hours", 22) == "22 oras" - assert self.locale._format_timeframe("hours", 25) == "25 oras" - - # Days - assert self.locale._format_timeframe("days", 0) == "0 araw" - assert self.locale._format_timeframe("days", 1) == "1 araw" - assert self.locale._format_timeframe("days", 2) == "2 araw" - assert self.locale._format_timeframe("days", 3) == "3 araw" - assert self.locale._format_timeframe("days", 21) == "21 araw" - - # Weeks - assert self.locale._format_timeframe("weeks", 0) == "0 linggo" - assert self.locale._format_timeframe("weeks", 1) == "1 linggo" - assert self.locale._format_timeframe("weeks", 2) == "2 linggo" - assert self.locale._format_timeframe("weeks", 4) == "4 linggo" - assert self.locale._format_timeframe("weeks", 5) == "5 linggo" - assert self.locale._format_timeframe("weeks", 21) == "21 linggo" - assert self.locale._format_timeframe("weeks", 22) == "22 linggo" - assert self.locale._format_timeframe("weeks", 25) == "25 linggo" - - # Months - assert self.locale._format_timeframe("months", 0) == "0 buwan" - assert self.locale._format_timeframe("months", 1) == "1 buwan" - assert self.locale._format_timeframe("months", 2) == "2 buwan" - assert self.locale._format_timeframe("months", 4) == "4 buwan" - assert self.locale._format_timeframe("months", 5) == "5 buwan" - assert self.locale._format_timeframe("months", 21) == "21 buwan" - assert self.locale._format_timeframe("months", 22) == "22 buwan" - assert self.locale._format_timeframe("months", 25) == "25 buwan" - - # Years - assert self.locale._format_timeframe("years", 1) == "1 taon" - assert self.locale._format_timeframe("years", 2) == "2 taon" - assert self.locale._format_timeframe("years", 5) == "5 taon" - - def test_multi_describe_tl(self): - describe = self.locale.describe_multi - - fulltest = [("years", 5), ("weeks", 1), ("hours", 1), ("minutes", 6)] - assert describe(fulltest) == "5 taon 1 linggo 1 oras 6 minuto mula ngayon" - seconds4000_0days = [("days", 0), ("hours", 1), ("minutes", 6)] - assert describe(seconds4000_0days) == "0 araw 1 oras 6 minuto mula ngayon" - seconds4000 = [("hours", 1), ("minutes", 6)] - assert describe(seconds4000) == "1 oras 6 minuto mula ngayon" - assert describe(seconds4000, only_distance=True) == "1 oras 6 minuto" - seconds3700 = [("hours", 1), ("minutes", 1)] - assert describe(seconds3700) == "1 oras 1 minuto mula ngayon" - seconds300_0hours = [("hours", 0), ("minutes", 5)] - assert describe(seconds300_0hours) == "0 oras 5 minuto mula ngayon" - seconds300 = [("minutes", 5)] - assert describe(seconds300) == "5 minuto mula ngayon" - seconds60 = [("minutes", 1)] - assert describe(seconds60) == "1 minuto mula ngayon" - assert describe(seconds60, only_distance=True) == "1 minuto" - seconds60 = [("seconds", 1)] - assert describe(seconds60) == "1 segundo mula ngayon" - assert describe(seconds60, only_distance=True) == "1 segundo" - - def test_ordinal_number_tl(self): - assert self.locale.ordinal_number(0) == "ika-0" - assert self.locale.ordinal_number(1) == "ika-1" - assert self.locale.ordinal_number(2) == "ika-2" - assert self.locale.ordinal_number(3) == "ika-3" - assert self.locale.ordinal_number(10) == "ika-10" - assert self.locale.ordinal_number(23) == "ika-23" - assert self.locale.ordinal_number(100) == "ika-100" - assert self.locale.ordinal_number(103) == "ika-103" - assert self.locale.ordinal_number(114) == "ika-114" - - -@pytest.mark.usefixtures("lang_locale") -class TestEstonianLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "just nüüd" - assert self.locale._format_timeframe("second", 1) == "ühe sekundi" - assert self.locale._format_timeframe("seconds", 3) == "3 sekundi" - assert self.locale._format_timeframe("seconds", 30) == "30 sekundi" - assert self.locale._format_timeframe("minute", 1) == "ühe minuti" - assert self.locale._format_timeframe("minutes", 4) == "4 minuti" - assert self.locale._format_timeframe("minutes", 40) == "40 minuti" - assert self.locale._format_timeframe("hour", 1) == "tunni aja" - assert self.locale._format_timeframe("hours", 5) == "5 tunni" - assert self.locale._format_timeframe("hours", 23) == "23 tunni" - assert self.locale._format_timeframe("day", 1) == "ühe päeva" - assert self.locale._format_timeframe("days", 6) == "6 päeva" - assert self.locale._format_timeframe("days", 12) == "12 päeva" - assert self.locale._format_timeframe("month", 1) == "ühe kuu" - assert self.locale._format_timeframe("months", 7) == "7 kuu" - assert self.locale._format_timeframe("months", 11) == "11 kuu" - assert self.locale._format_timeframe("year", 1) == "ühe aasta" - assert self.locale._format_timeframe("years", 8) == "8 aasta" - assert self.locale._format_timeframe("years", 12) == "12 aasta" - - assert self.locale._format_timeframe("now", 0) == "just nüüd" - assert self.locale._format_timeframe("second", -1) == "üks sekund" - assert self.locale._format_timeframe("seconds", -9) == "9 sekundit" - assert self.locale._format_timeframe("seconds", -12) == "12 sekundit" - assert self.locale._format_timeframe("minute", -1) == "üks minut" - assert self.locale._format_timeframe("minutes", -2) == "2 minutit" - assert self.locale._format_timeframe("minutes", -10) == "10 minutit" - assert self.locale._format_timeframe("hour", -1) == "tund aega" - assert self.locale._format_timeframe("hours", -3) == "3 tundi" - assert self.locale._format_timeframe("hours", -11) == "11 tundi" - assert self.locale._format_timeframe("day", -1) == "üks päev" - assert self.locale._format_timeframe("days", -2) == "2 päeva" - assert self.locale._format_timeframe("days", -12) == "12 päeva" - assert self.locale._format_timeframe("month", -1) == "üks kuu" - assert self.locale._format_timeframe("months", -3) == "3 kuud" - assert self.locale._format_timeframe("months", -13) == "13 kuud" - assert self.locale._format_timeframe("year", -1) == "üks aasta" - assert self.locale._format_timeframe("years", -4) == "4 aastat" - assert self.locale._format_timeframe("years", -14) == "14 aastat" - - -@pytest.mark.usefixtures("lang_locale") -class TestPortugueseLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "agora" - assert self.locale._format_timeframe("second", 1) == "um segundo" - assert self.locale._format_timeframe("seconds", 30) == "30 segundos" - assert self.locale._format_timeframe("minute", 1) == "um minuto" - assert self.locale._format_timeframe("minutes", 40) == "40 minutos" - assert self.locale._format_timeframe("hour", 1) == "uma hora" - assert self.locale._format_timeframe("hours", 23) == "23 horas" - assert self.locale._format_timeframe("day", 1) == "um dia" - assert self.locale._format_timeframe("days", 12) == "12 dias" - assert self.locale._format_timeframe("month", 1) == "um mês" - assert self.locale._format_timeframe("months", 11) == "11 meses" - assert self.locale._format_timeframe("year", 1) == "um ano" - assert self.locale._format_timeframe("years", 12) == "12 anos" - - -@pytest.mark.usefixtures("lang_locale") -class TestBrazilianPortugueseLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "agora" - assert self.locale._format_timeframe("second", 1) == "um segundo" - assert self.locale._format_timeframe("seconds", 30) == "30 segundos" - assert self.locale._format_timeframe("minute", 1) == "um minuto" - assert self.locale._format_timeframe("minutes", 40) == "40 minutos" - assert self.locale._format_timeframe("hour", 1) == "uma hora" - assert self.locale._format_timeframe("hours", 23) == "23 horas" - assert self.locale._format_timeframe("day", 1) == "um dia" - assert self.locale._format_timeframe("days", 12) == "12 dias" - assert self.locale._format_timeframe("month", 1) == "um mês" - assert self.locale._format_timeframe("months", 11) == "11 meses" - assert self.locale._format_timeframe("year", 1) == "um ano" - assert self.locale._format_timeframe("years", 12) == "12 anos" - assert self.locale._format_relative("uma hora", "hour", -1) == "faz uma hora" - - -@pytest.mark.usefixtures("lang_locale") -class TestHongKongLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "剛才" - assert self.locale._format_timeframe("second", 1) == "1秒" - assert self.locale._format_timeframe("seconds", 30) == "30秒" - assert self.locale._format_timeframe("minute", 1) == "1分鐘" - assert self.locale._format_timeframe("minutes", 40) == "40分鐘" - assert self.locale._format_timeframe("hour", 1) == "1小時" - assert self.locale._format_timeframe("hours", 23) == "23小時" - assert self.locale._format_timeframe("day", 1) == "1天" - assert self.locale._format_timeframe("days", 12) == "12天" - assert self.locale._format_timeframe("week", 1) == "1星期" - assert self.locale._format_timeframe("weeks", 38) == "38星期" - assert self.locale._format_timeframe("month", 1) == "1個月" - assert self.locale._format_timeframe("months", 11) == "11個月" - assert self.locale._format_timeframe("year", 1) == "1年" - assert self.locale._format_timeframe("years", 12) == "12年" - - -@pytest.mark.usefixtures("lang_locale") -class TestChineseTWLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "剛才" - assert self.locale._format_timeframe("second", 1) == "1秒" - assert self.locale._format_timeframe("seconds", 30) == "30秒" - assert self.locale._format_timeframe("minute", 1) == "1分鐘" - assert self.locale._format_timeframe("minutes", 40) == "40分鐘" - assert self.locale._format_timeframe("hour", 1) == "1小時" - assert self.locale._format_timeframe("hours", 23) == "23小時" - assert self.locale._format_timeframe("day", 1) == "1天" - assert self.locale._format_timeframe("days", 12) == "12天" - assert self.locale._format_timeframe("week", 1) == "1週" - assert self.locale._format_timeframe("weeks", 38) == "38週" - assert self.locale._format_timeframe("month", 1) == "1個月" - assert self.locale._format_timeframe("months", 11) == "11個月" - assert self.locale._format_timeframe("year", 1) == "1年" - assert self.locale._format_timeframe("years", 12) == "12年" - - -@pytest.mark.usefixtures("lang_locale") -class TestSwahiliLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "sasa hivi" - assert self.locale._format_timeframe("second", 1) == "sekunde" - assert self.locale._format_timeframe("seconds", 3) == "sekunde 3" - assert self.locale._format_timeframe("seconds", 30) == "sekunde 30" - assert self.locale._format_timeframe("minute", 1) == "dakika moja" - assert self.locale._format_timeframe("minutes", 4) == "dakika 4" - assert self.locale._format_timeframe("minutes", 40) == "dakika 40" - assert self.locale._format_timeframe("hour", 1) == "saa moja" - assert self.locale._format_timeframe("hours", 5) == "saa 5" - assert self.locale._format_timeframe("hours", 23) == "saa 23" - assert self.locale._format_timeframe("day", 1) == "siku moja" - assert self.locale._format_timeframe("days", 6) == "siku 6" - assert self.locale._format_timeframe("days", 12) == "siku 12" - assert self.locale._format_timeframe("month", 1) == "mwezi moja" - assert self.locale._format_timeframe("months", 7) == "miezi 7" - assert self.locale._format_timeframe("week", 1) == "wiki moja" - assert self.locale._format_timeframe("weeks", 2) == "wiki 2" - assert self.locale._format_timeframe("months", 11) == "miezi 11" - assert self.locale._format_timeframe("year", 1) == "mwaka moja" - assert self.locale._format_timeframe("years", 8) == "miaka 8" - assert self.locale._format_timeframe("years", 12) == "miaka 12" - - def test_format_relative_now(self): - result = self.locale._format_relative("sasa hivi", "now", 0) - assert result == "sasa hivi" - - def test_format_relative_past(self): - result = self.locale._format_relative("saa moja", "hour", 1) - assert result == "muda wa saa moja" - - def test_format_relative_future(self): - result = self.locale._format_relative("saa moja", "hour", -1) - assert result == "saa moja iliyopita" - - -@pytest.mark.usefixtures("lang_locale") -class TestKoreanLocale: - def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "지금" - assert self.locale._format_timeframe("second", 1) == "1초" - assert self.locale._format_timeframe("seconds", 2) == "2초" - assert self.locale._format_timeframe("minute", 1) == "1분" - assert self.locale._format_timeframe("minutes", 2) == "2분" - assert self.locale._format_timeframe("hour", 1) == "한시간" - assert self.locale._format_timeframe("hours", 2) == "2시간" - assert self.locale._format_timeframe("day", 1) == "하루" - assert self.locale._format_timeframe("days", 2) == "2일" - assert self.locale._format_timeframe("week", 1) == "1주" - assert self.locale._format_timeframe("weeks", 2) == "2주" - assert self.locale._format_timeframe("month", 1) == "한달" - assert self.locale._format_timeframe("months", 2) == "2개월" - assert self.locale._format_timeframe("year", 1) == "1년" - assert self.locale._format_timeframe("years", 2) == "2년" - - def test_format_relative(self): - assert self.locale._format_relative("지금", "now", 0) == "지금" - - assert self.locale._format_relative("1초", "second", 1) == "1초 후" - assert self.locale._format_relative("2초", "seconds", 2) == "2초 후" - assert self.locale._format_relative("1분", "minute", 1) == "1분 후" - assert self.locale._format_relative("2분", "minutes", 2) == "2분 후" - assert self.locale._format_relative("한시간", "hour", 1) == "한시간 후" - assert self.locale._format_relative("2시간", "hours", 2) == "2시간 후" - assert self.locale._format_relative("하루", "day", 1) == "내일" - assert self.locale._format_relative("2일", "days", 2) == "모레" - assert self.locale._format_relative("3일", "days", 3) == "글피" - assert self.locale._format_relative("4일", "days", 4) == "그글피" - assert self.locale._format_relative("5일", "days", 5) == "5일 후" - assert self.locale._format_relative("1주", "week", 1) == "1주 후" - assert self.locale._format_relative("2주", "weeks", 2) == "2주 후" - assert self.locale._format_relative("한달", "month", 1) == "한달 후" - assert self.locale._format_relative("2개월", "months", 2) == "2개월 후" - assert self.locale._format_relative("1년", "year", 1) == "내년" - assert self.locale._format_relative("2년", "years", 2) == "내후년" - assert self.locale._format_relative("3년", "years", 3) == "3년 후" - - assert self.locale._format_relative("1초", "second", -1) == "1초 전" - assert self.locale._format_relative("2초", "seconds", -2) == "2초 전" - assert self.locale._format_relative("1분", "minute", -1) == "1분 전" - assert self.locale._format_relative("2분", "minutes", -2) == "2분 전" - assert self.locale._format_relative("한시간", "hour", -1) == "한시간 전" - assert self.locale._format_relative("2시간", "hours", -2) == "2시간 전" - assert self.locale._format_relative("하루", "day", -1) == "어제" - assert self.locale._format_relative("2일", "days", -2) == "그제" - assert self.locale._format_relative("3일", "days", -3) == "그끄제" - assert self.locale._format_relative("4일", "days", -4) == "4일 전" - assert self.locale._format_relative("1주", "week", -1) == "1주 전" - assert self.locale._format_relative("2주", "weeks", -2) == "2주 전" - assert self.locale._format_relative("한달", "month", -1) == "한달 전" - assert self.locale._format_relative("2개월", "months", -2) == "2개월 전" - assert self.locale._format_relative("1년", "year", -1) == "작년" - assert self.locale._format_relative("2년", "years", -2) == "제작년" - assert self.locale._format_relative("3년", "years", -3) == "3년 전" - - def test_ordinal_number(self): - assert self.locale.ordinal_number(0) == "0번째" - assert self.locale.ordinal_number(1) == "첫번째" - assert self.locale.ordinal_number(2) == "두번째" - assert self.locale.ordinal_number(3) == "세번째" - assert self.locale.ordinal_number(4) == "네번째" - assert self.locale.ordinal_number(5) == "다섯번째" - assert self.locale.ordinal_number(6) == "여섯번째" - assert self.locale.ordinal_number(7) == "일곱번째" - assert self.locale.ordinal_number(8) == "여덟번째" - assert self.locale.ordinal_number(9) == "아홉번째" - assert self.locale.ordinal_number(10) == "열번째" - assert self.locale.ordinal_number(11) == "11번째" - assert self.locale.ordinal_number(12) == "12번째" - assert self.locale.ordinal_number(100) == "100번째" diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_parser.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/test_parser.py deleted file mode 100644 index 9fb4e68f3c..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_parser.py +++ /dev/null @@ -1,1657 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import calendar -import os -import time -from datetime import datetime - -import pytest -from dateutil import tz - -import arrow -from arrow import formatter, parser -from arrow.constants import MAX_TIMESTAMP_US -from arrow.parser import DateTimeParser, ParserError, ParserMatchError - -from .utils import make_full_tz_list - - -@pytest.mark.usefixtures("dt_parser") -class TestDateTimeParser: - def test_parse_multiformat(self, mocker): - mocker.patch( - "arrow.parser.DateTimeParser.parse", - string="str", - fmt="fmt_a", - side_effect=parser.ParserError, - ) - - with pytest.raises(parser.ParserError): - self.parser._parse_multiformat("str", ["fmt_a"]) - - mock_datetime = mocker.Mock() - mocker.patch( - "arrow.parser.DateTimeParser.parse", - string="str", - fmt="fmt_b", - return_value=mock_datetime, - ) - - result = self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - assert result == mock_datetime - - def test_parse_multiformat_all_fail(self, mocker): - mocker.patch( - "arrow.parser.DateTimeParser.parse", - string="str", - fmt="fmt_a", - side_effect=parser.ParserError, - ) - - mocker.patch( - "arrow.parser.DateTimeParser.parse", - string="str", - fmt="fmt_b", - side_effect=parser.ParserError, - ) - - with pytest.raises(parser.ParserError): - self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - - def test_parse_multiformat_unself_expected_fail(self, mocker): - class UnselfExpectedError(Exception): - pass - - mocker.patch( - "arrow.parser.DateTimeParser.parse", - string="str", - fmt="fmt_a", - side_effect=UnselfExpectedError, - ) - - with pytest.raises(UnselfExpectedError): - self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - - def test_parse_token_nonsense(self): - parts = {} - self.parser._parse_token("NONSENSE", "1900", parts) - assert parts == {} - - def test_parse_token_invalid_meridians(self): - parts = {} - self.parser._parse_token("A", "a..m", parts) - assert parts == {} - self.parser._parse_token("a", "p..m", parts) - assert parts == {} - - def test_parser_no_caching(self, mocker): - - mocked_parser = mocker.patch( - "arrow.parser.DateTimeParser._generate_pattern_re", fmt="fmt_a" - ) - self.parser = parser.DateTimeParser(cache_size=0) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - assert mocked_parser.call_count == 100 - - def test_parser_1_line_caching(self, mocker): - mocked_parser = mocker.patch("arrow.parser.DateTimeParser._generate_pattern_re") - self.parser = parser.DateTimeParser(cache_size=1) - - for _ in range(100): - self.parser._generate_pattern_re(fmt="fmt_a") - assert mocked_parser.call_count == 1 - assert mocked_parser.call_args_list[0] == mocker.call(fmt="fmt_a") - - for _ in range(100): - self.parser._generate_pattern_re(fmt="fmt_b") - assert mocked_parser.call_count == 2 - assert mocked_parser.call_args_list[1] == mocker.call(fmt="fmt_b") - - for _ in range(100): - self.parser._generate_pattern_re(fmt="fmt_a") - assert mocked_parser.call_count == 3 - assert mocked_parser.call_args_list[2] == mocker.call(fmt="fmt_a") - - def test_parser_multiple_line_caching(self, mocker): - mocked_parser = mocker.patch("arrow.parser.DateTimeParser._generate_pattern_re") - self.parser = parser.DateTimeParser(cache_size=2) - - for _ in range(100): - self.parser._generate_pattern_re(fmt="fmt_a") - assert mocked_parser.call_count == 1 - assert mocked_parser.call_args_list[0] == mocker.call(fmt="fmt_a") - - for _ in range(100): - self.parser._generate_pattern_re(fmt="fmt_b") - assert mocked_parser.call_count == 2 - assert mocked_parser.call_args_list[1] == mocker.call(fmt="fmt_b") - - # fmt_a and fmt_b are in the cache, so no new calls should be made - for _ in range(100): - self.parser._generate_pattern_re(fmt="fmt_a") - for _ in range(100): - self.parser._generate_pattern_re(fmt="fmt_b") - assert mocked_parser.call_count == 2 - assert mocked_parser.call_args_list[0] == mocker.call(fmt="fmt_a") - assert mocked_parser.call_args_list[1] == mocker.call(fmt="fmt_b") - - def test_YY_and_YYYY_format_list(self): - - assert self.parser.parse("15/01/19", ["DD/MM/YY", "DD/MM/YYYY"]) == datetime( - 2019, 1, 15 - ) - - # Regression test for issue #580 - assert self.parser.parse("15/01/2019", ["DD/MM/YY", "DD/MM/YYYY"]) == datetime( - 2019, 1, 15 - ) - - assert ( - self.parser.parse( - "15/01/2019T04:05:06.789120Z", - ["D/M/YYThh:mm:ss.SZ", "D/M/YYYYThh:mm:ss.SZ"], - ) - == datetime(2019, 1, 15, 4, 5, 6, 789120, tzinfo=tz.tzutc()) - ) - - # regression test for issue #447 - def test_timestamp_format_list(self): - # should not match on the "X" token - assert ( - self.parser.parse( - "15 Jul 2000", - ["MM/DD/YYYY", "YYYY-MM-DD", "X", "DD-MMMM-YYYY", "D MMM YYYY"], - ) - == datetime(2000, 7, 15) - ) - - with pytest.raises(ParserError): - self.parser.parse("15 Jul", "X") - - -@pytest.mark.usefixtures("dt_parser") -class TestDateTimeParserParse: - def test_parse_list(self, mocker): - - mocker.patch( - "arrow.parser.DateTimeParser._parse_multiformat", - string="str", - formats=["fmt_a", "fmt_b"], - return_value="result", - ) - - result = self.parser.parse("str", ["fmt_a", "fmt_b"]) - assert result == "result" - - def test_parse_unrecognized_token(self, mocker): - - mocker.patch.dict("arrow.parser.DateTimeParser._BASE_INPUT_RE_MAP") - del arrow.parser.DateTimeParser._BASE_INPUT_RE_MAP["YYYY"] - - # need to make another local parser to apply patch changes - _parser = parser.DateTimeParser() - with pytest.raises(parser.ParserError): - _parser.parse("2013-01-01", "YYYY-MM-DD") - - def test_parse_parse_no_match(self): - - with pytest.raises(ParserError): - self.parser.parse("01-01", "YYYY-MM-DD") - - def test_parse_separators(self): - - with pytest.raises(ParserError): - self.parser.parse("1403549231", "YYYY-MM-DD") - - def test_parse_numbers(self): - - self.expected = datetime(2012, 1, 1, 12, 5, 10) - assert ( - self.parser.parse("2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss") - == self.expected - ) - - def test_parse_year_two_digit(self): - - self.expected = datetime(1979, 1, 1, 12, 5, 10) - assert ( - self.parser.parse("79-01-01 12:05:10", "YY-MM-DD HH:mm:ss") == self.expected - ) - - def test_parse_timestamp(self): - - tz_utc = tz.tzutc() - int_timestamp = int(time.time()) - self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) - assert self.parser.parse("{:d}".format(int_timestamp), "X") == self.expected - - float_timestamp = time.time() - self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - assert self.parser.parse("{:f}".format(float_timestamp), "X") == self.expected - - # test handling of ns timestamp (arrow will round to 6 digits regardless) - self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - assert ( - self.parser.parse("{:f}123".format(float_timestamp), "X") == self.expected - ) - - # test ps timestamp (arrow will round to 6 digits regardless) - self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - assert ( - self.parser.parse("{:f}123456".format(float_timestamp), "X") - == self.expected - ) - - # NOTE: negative timestamps cannot be handled by datetime on Window - # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 - if os.name != "nt": - # regression test for issue #662 - negative_int_timestamp = -int_timestamp - self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) - assert ( - self.parser.parse("{:d}".format(negative_int_timestamp), "X") - == self.expected - ) - - negative_float_timestamp = -float_timestamp - self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) - assert ( - self.parser.parse("{:f}".format(negative_float_timestamp), "X") - == self.expected - ) - - # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will - # break cases like "15 Jul 2000" and a format list (see issue #447) - with pytest.raises(ParserError): - natural_lang_string = "Meet me at {} at the restaurant.".format( - float_timestamp - ) - self.parser.parse(natural_lang_string, "X") - - with pytest.raises(ParserError): - self.parser.parse("1565982019.", "X") - - with pytest.raises(ParserError): - self.parser.parse(".1565982019", "X") - - def test_parse_expanded_timestamp(self): - # test expanded timestamps that include milliseconds - # and microseconds as multiples rather than decimals - # requested in issue #357 - - tz_utc = tz.tzutc() - timestamp = 1569982581.413132 - timestamp_milli = int(round(timestamp * 1000)) - timestamp_micro = int(round(timestamp * 1000000)) - - # "x" token should parse integer timestamps below MAX_TIMESTAMP normally - self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) - assert self.parser.parse("{:d}".format(int(timestamp)), "x") == self.expected - - self.expected = datetime.fromtimestamp(round(timestamp, 3), tz=tz_utc) - assert self.parser.parse("{:d}".format(timestamp_milli), "x") == self.expected - - self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) - assert self.parser.parse("{:d}".format(timestamp_micro), "x") == self.expected - - # anything above max µs timestamp should fail - with pytest.raises(ValueError): - self.parser.parse("{:d}".format(int(MAX_TIMESTAMP_US) + 1), "x") - - # floats are not allowed with the "x" token - with pytest.raises(ParserMatchError): - self.parser.parse("{:f}".format(timestamp), "x") - - def test_parse_names(self): - - self.expected = datetime(2012, 1, 1) - - assert self.parser.parse("January 1, 2012", "MMMM D, YYYY") == self.expected - assert self.parser.parse("Jan 1, 2012", "MMM D, YYYY") == self.expected - - def test_parse_pm(self): - - self.expected = datetime(1, 1, 1, 13, 0, 0) - assert self.parser.parse("1 pm", "H a") == self.expected - assert self.parser.parse("1 pm", "h a") == self.expected - - self.expected = datetime(1, 1, 1, 1, 0, 0) - assert self.parser.parse("1 am", "H A") == self.expected - assert self.parser.parse("1 am", "h A") == self.expected - - self.expected = datetime(1, 1, 1, 0, 0, 0) - assert self.parser.parse("12 am", "H A") == self.expected - assert self.parser.parse("12 am", "h A") == self.expected - - self.expected = datetime(1, 1, 1, 12, 0, 0) - assert self.parser.parse("12 pm", "H A") == self.expected - assert self.parser.parse("12 pm", "h A") == self.expected - - def test_parse_tz_hours_only(self): - - self.expected = datetime(2025, 10, 17, 5, 30, 10, tzinfo=tz.tzoffset(None, 0)) - parsed = self.parser.parse("2025-10-17 05:30:10+00", "YYYY-MM-DD HH:mm:ssZ") - assert parsed == self.expected - - def test_parse_tz_zz(self): - - self.expected = datetime(2013, 1, 1, tzinfo=tz.tzoffset(None, -7 * 3600)) - assert self.parser.parse("2013-01-01 -07:00", "YYYY-MM-DD ZZ") == self.expected - - @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) - def test_parse_tz_name_zzz(self, full_tz_name): - - self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(full_tz_name)) - assert ( - self.parser.parse("2013-01-01 {}".format(full_tz_name), "YYYY-MM-DD ZZZ") - == self.expected - ) - - # note that offsets are not timezones - with pytest.raises(ParserError): - self.parser.parse("2013-01-01 12:30:45.9+1000", "YYYY-MM-DDZZZ") - - with pytest.raises(ParserError): - self.parser.parse("2013-01-01 12:30:45.9+10:00", "YYYY-MM-DDZZZ") - - with pytest.raises(ParserError): - self.parser.parse("2013-01-01 12:30:45.9-10", "YYYY-MM-DDZZZ") - - def test_parse_subsecond(self): - self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) - assert ( - self.parser.parse("2013-01-01 12:30:45.9", "YYYY-MM-DD HH:mm:ss.S") - == self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) - assert ( - self.parser.parse("2013-01-01 12:30:45.98", "YYYY-MM-DD HH:mm:ss.SS") - == self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) - assert ( - self.parser.parse("2013-01-01 12:30:45.987", "YYYY-MM-DD HH:mm:ss.SSS") - == self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) - assert ( - self.parser.parse("2013-01-01 12:30:45.9876", "YYYY-MM-DD HH:mm:ss.SSSS") - == self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) - assert ( - self.parser.parse("2013-01-01 12:30:45.98765", "YYYY-MM-DD HH:mm:ss.SSSSS") - == self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - assert ( - self.parser.parse( - "2013-01-01 12:30:45.987654", "YYYY-MM-DD HH:mm:ss.SSSSSS" - ) - == self.expected - ) - - def test_parse_subsecond_rounding(self): - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - datetime_format = "YYYY-MM-DD HH:mm:ss.S" - - # round up - string = "2013-01-01 12:30:45.9876539" - assert self.parser.parse(string, datetime_format) == self.expected - assert self.parser.parse_iso(string) == self.expected - - # round down - string = "2013-01-01 12:30:45.98765432" - assert self.parser.parse(string, datetime_format) == self.expected - assert self.parser.parse_iso(string) == self.expected - - # round half-up - string = "2013-01-01 12:30:45.987653521" - assert self.parser.parse(string, datetime_format) == self.expected - assert self.parser.parse_iso(string) == self.expected - - # round half-down - string = "2013-01-01 12:30:45.9876545210" - assert self.parser.parse(string, datetime_format) == self.expected - assert self.parser.parse_iso(string) == self.expected - - # overflow (zero out the subseconds and increment the seconds) - # regression tests for issue #636 - def test_parse_subsecond_rounding_overflow(self): - datetime_format = "YYYY-MM-DD HH:mm:ss.S" - - self.expected = datetime(2013, 1, 1, 12, 30, 46) - string = "2013-01-01 12:30:45.9999995" - assert self.parser.parse(string, datetime_format) == self.expected - assert self.parser.parse_iso(string) == self.expected - - self.expected = datetime(2013, 1, 1, 12, 31, 0) - string = "2013-01-01 12:30:59.9999999" - assert self.parser.parse(string, datetime_format) == self.expected - assert self.parser.parse_iso(string) == self.expected - - self.expected = datetime(2013, 1, 2, 0, 0, 0) - string = "2013-01-01 23:59:59.9999999" - assert self.parser.parse(string, datetime_format) == self.expected - assert self.parser.parse_iso(string) == self.expected - - # 6 digits should remain unrounded - self.expected = datetime(2013, 1, 1, 12, 30, 45, 999999) - string = "2013-01-01 12:30:45.999999" - assert self.parser.parse(string, datetime_format) == self.expected - assert self.parser.parse_iso(string) == self.expected - - # Regression tests for issue #560 - def test_parse_long_year(self): - with pytest.raises(ParserError): - self.parser.parse("09 January 123456789101112", "DD MMMM YYYY") - - with pytest.raises(ParserError): - self.parser.parse("123456789101112 09 January", "YYYY DD MMMM") - - with pytest.raises(ParserError): - self.parser.parse("68096653015/01/19", "YY/M/DD") - - def test_parse_with_extra_words_at_start_and_end_invalid(self): - input_format_pairs = [ - ("blah2016", "YYYY"), - ("blah2016blah", "YYYY"), - ("2016blah", "YYYY"), - ("2016-05blah", "YYYY-MM"), - ("2016-05-16blah", "YYYY-MM-DD"), - ("2016-05-16T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), - ("2016-05-16T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05-16T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05-16T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ] - - for pair in input_format_pairs: - with pytest.raises(ParserError): - self.parser.parse(pair[0], pair[1]) - - def test_parse_with_extra_words_at_start_and_end_valid(self): - # Spaces surrounding the parsable date are ok because we - # allow the parsing of natural language input. Additionally, a single - # character of specific punctuation before or after the date is okay. - # See docs for full list of valid punctuation. - - assert self.parser.parse("blah 2016 blah", "YYYY") == datetime(2016, 1, 1) - - assert self.parser.parse("blah 2016", "YYYY") == datetime(2016, 1, 1) - - assert self.parser.parse("2016 blah", "YYYY") == datetime(2016, 1, 1) - - # test one additional space along with space divider - assert self.parser.parse( - "blah 2016-05-16 04:05:06.789120", "YYYY-MM-DD hh:mm:ss.S" - ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - - assert self.parser.parse( - "2016-05-16 04:05:06.789120 blah", "YYYY-MM-DD hh:mm:ss.S" - ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - - # test one additional space along with T divider - assert self.parser.parse( - "blah 2016-05-16T04:05:06.789120", "YYYY-MM-DDThh:mm:ss.S" - ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - - assert self.parser.parse( - "2016-05-16T04:05:06.789120 blah", "YYYY-MM-DDThh:mm:ss.S" - ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - - assert ( - self.parser.parse( - "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", - "YYYY-MM-DDThh:mm:ss.S", - ) - == datetime(2016, 5, 16, 4, 5, 6, 789120) - ) - - assert ( - self.parser.parse( - "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", - "YYYY-MM-DD hh:mm:ss.S", - ) - == datetime(2016, 5, 16, 4, 5, 6, 789120) - ) - - # regression test for issue #701 - # tests cases of a partial match surrounded by punctuation - # for the list of valid punctuation, see documentation - def test_parse_with_punctuation_fences(self): - assert self.parser.parse( - "Meet me at my house on Halloween (2019-31-10)", "YYYY-DD-MM" - ) == datetime(2019, 10, 31) - - assert self.parser.parse( - "Monday, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY" - ) == datetime(2019, 9, 9) - - assert self.parser.parse("A date is 11.11.2011.", "DD.MM.YYYY") == datetime( - 2011, 11, 11 - ) - - with pytest.raises(ParserMatchError): - self.parser.parse("11.11.2011.1 is not a valid date.", "DD.MM.YYYY") - - with pytest.raises(ParserMatchError): - self.parser.parse( - "This date has too many punctuation marks following it (11.11.2011).", - "DD.MM.YYYY", - ) - - def test_parse_with_leading_and_trailing_whitespace(self): - assert self.parser.parse(" 2016", "YYYY") == datetime(2016, 1, 1) - - assert self.parser.parse("2016 ", "YYYY") == datetime(2016, 1, 1) - - assert self.parser.parse(" 2016 ", "YYYY") == datetime(2016, 1, 1) - - assert self.parser.parse( - " 2016-05-16 04:05:06.789120 ", "YYYY-MM-DD hh:mm:ss.S" - ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - - assert self.parser.parse( - " 2016-05-16T04:05:06.789120 ", "YYYY-MM-DDThh:mm:ss.S" - ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - - def test_parse_YYYY_DDDD(self): - assert self.parser.parse("1998-136", "YYYY-DDDD") == datetime(1998, 5, 16) - - assert self.parser.parse("1998-006", "YYYY-DDDD") == datetime(1998, 1, 6) - - with pytest.raises(ParserError): - self.parser.parse("1998-456", "YYYY-DDDD") - - def test_parse_YYYY_DDD(self): - assert self.parser.parse("1998-6", "YYYY-DDD") == datetime(1998, 1, 6) - - assert self.parser.parse("1998-136", "YYYY-DDD") == datetime(1998, 5, 16) - - with pytest.raises(ParserError): - self.parser.parse("1998-756", "YYYY-DDD") - - # month cannot be passed with DDD and DDDD tokens - def test_parse_YYYY_MM_DDDD(self): - with pytest.raises(ParserError): - self.parser.parse("2015-01-009", "YYYY-MM-DDDD") - - # year is required with the DDD and DDDD tokens - def test_parse_DDD_only(self): - with pytest.raises(ParserError): - self.parser.parse("5", "DDD") - - def test_parse_DDDD_only(self): - with pytest.raises(ParserError): - self.parser.parse("145", "DDDD") - - def test_parse_ddd_and_dddd(self): - fr_parser = parser.DateTimeParser("fr") - - # Day of week should be ignored when a day is passed - # 2019-10-17 is a Thursday, so we know day of week - # is ignored if the same date is outputted - expected = datetime(2019, 10, 17) - assert self.parser.parse("Tue 2019-10-17", "ddd YYYY-MM-DD") == expected - assert fr_parser.parse("mar 2019-10-17", "ddd YYYY-MM-DD") == expected - assert self.parser.parse("Tuesday 2019-10-17", "dddd YYYY-MM-DD") == expected - assert fr_parser.parse("mardi 2019-10-17", "dddd YYYY-MM-DD") == expected - - # Get first Tuesday after epoch - expected = datetime(1970, 1, 6) - assert self.parser.parse("Tue", "ddd") == expected - assert fr_parser.parse("mar", "ddd") == expected - assert self.parser.parse("Tuesday", "dddd") == expected - assert fr_parser.parse("mardi", "dddd") == expected - - # Get first Tuesday in 2020 - expected = datetime(2020, 1, 7) - assert self.parser.parse("Tue 2020", "ddd YYYY") == expected - assert fr_parser.parse("mar 2020", "ddd YYYY") == expected - assert self.parser.parse("Tuesday 2020", "dddd YYYY") == expected - assert fr_parser.parse("mardi 2020", "dddd YYYY") == expected - - # Get first Tuesday in February 2020 - expected = datetime(2020, 2, 4) - assert self.parser.parse("Tue 02 2020", "ddd MM YYYY") == expected - assert fr_parser.parse("mar 02 2020", "ddd MM YYYY") == expected - assert self.parser.parse("Tuesday 02 2020", "dddd MM YYYY") == expected - assert fr_parser.parse("mardi 02 2020", "dddd MM YYYY") == expected - - # Get first Tuesday in February after epoch - expected = datetime(1970, 2, 3) - assert self.parser.parse("Tue 02", "ddd MM") == expected - assert fr_parser.parse("mar 02", "ddd MM") == expected - assert self.parser.parse("Tuesday 02", "dddd MM") == expected - assert fr_parser.parse("mardi 02", "dddd MM") == expected - - # Times remain intact - expected = datetime(2020, 2, 4, 10, 25, 54, 123456, tz.tzoffset(None, -3600)) - assert ( - self.parser.parse( - "Tue 02 2020 10:25:54.123456-01:00", "ddd MM YYYY HH:mm:ss.SZZ" - ) - == expected - ) - assert ( - fr_parser.parse( - "mar 02 2020 10:25:54.123456-01:00", "ddd MM YYYY HH:mm:ss.SZZ" - ) - == expected - ) - assert ( - self.parser.parse( - "Tuesday 02 2020 10:25:54.123456-01:00", "dddd MM YYYY HH:mm:ss.SZZ" - ) - == expected - ) - assert ( - fr_parser.parse( - "mardi 02 2020 10:25:54.123456-01:00", "dddd MM YYYY HH:mm:ss.SZZ" - ) - == expected - ) - - def test_parse_ddd_and_dddd_ignore_case(self): - # Regression test for issue #851 - expected = datetime(2019, 6, 24) - assert ( - self.parser.parse("MONDAY, June 24, 2019", "dddd, MMMM DD, YYYY") - == expected - ) - - def test_parse_ddd_and_dddd_then_format(self): - # Regression test for issue #446 - arw_formatter = formatter.DateTimeFormatter() - assert arw_formatter.format(self.parser.parse("Mon", "ddd"), "ddd") == "Mon" - assert ( - arw_formatter.format(self.parser.parse("Monday", "dddd"), "dddd") - == "Monday" - ) - assert arw_formatter.format(self.parser.parse("Tue", "ddd"), "ddd") == "Tue" - assert ( - arw_formatter.format(self.parser.parse("Tuesday", "dddd"), "dddd") - == "Tuesday" - ) - assert arw_formatter.format(self.parser.parse("Wed", "ddd"), "ddd") == "Wed" - assert ( - arw_formatter.format(self.parser.parse("Wednesday", "dddd"), "dddd") - == "Wednesday" - ) - assert arw_formatter.format(self.parser.parse("Thu", "ddd"), "ddd") == "Thu" - assert ( - arw_formatter.format(self.parser.parse("Thursday", "dddd"), "dddd") - == "Thursday" - ) - assert arw_formatter.format(self.parser.parse("Fri", "ddd"), "ddd") == "Fri" - assert ( - arw_formatter.format(self.parser.parse("Friday", "dddd"), "dddd") - == "Friday" - ) - assert arw_formatter.format(self.parser.parse("Sat", "ddd"), "ddd") == "Sat" - assert ( - arw_formatter.format(self.parser.parse("Saturday", "dddd"), "dddd") - == "Saturday" - ) - assert arw_formatter.format(self.parser.parse("Sun", "ddd"), "ddd") == "Sun" - assert ( - arw_formatter.format(self.parser.parse("Sunday", "dddd"), "dddd") - == "Sunday" - ) - - def test_parse_HH_24(self): - assert self.parser.parse( - "2019-10-30T24:00:00", "YYYY-MM-DDTHH:mm:ss" - ) == datetime(2019, 10, 31, 0, 0, 0, 0) - assert self.parser.parse("2019-10-30T24:00", "YYYY-MM-DDTHH:mm") == datetime( - 2019, 10, 31, 0, 0, 0, 0 - ) - assert self.parser.parse("2019-10-30T24", "YYYY-MM-DDTHH") == datetime( - 2019, 10, 31, 0, 0, 0, 0 - ) - assert self.parser.parse( - "2019-10-30T24:00:00.0", "YYYY-MM-DDTHH:mm:ss.S" - ) == datetime(2019, 10, 31, 0, 0, 0, 0) - assert self.parser.parse( - "2019-10-31T24:00:00", "YYYY-MM-DDTHH:mm:ss" - ) == datetime(2019, 11, 1, 0, 0, 0, 0) - assert self.parser.parse( - "2019-12-31T24:00:00", "YYYY-MM-DDTHH:mm:ss" - ) == datetime(2020, 1, 1, 0, 0, 0, 0) - assert self.parser.parse( - "2019-12-31T23:59:59.9999999", "YYYY-MM-DDTHH:mm:ss.S" - ) == datetime(2020, 1, 1, 0, 0, 0, 0) - - with pytest.raises(ParserError): - self.parser.parse("2019-12-31T24:01:00", "YYYY-MM-DDTHH:mm:ss") - - with pytest.raises(ParserError): - self.parser.parse("2019-12-31T24:00:01", "YYYY-MM-DDTHH:mm:ss") - - with pytest.raises(ParserError): - self.parser.parse("2019-12-31T24:00:00.1", "YYYY-MM-DDTHH:mm:ss.S") - - with pytest.raises(ParserError): - self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S") - - def test_parse_W(self): - - assert self.parser.parse("2011-W05-4", "W") == datetime(2011, 2, 3) - assert self.parser.parse("2011W054", "W") == datetime(2011, 2, 3) - assert self.parser.parse("2011-W05", "W") == datetime(2011, 1, 31) - assert self.parser.parse("2011W05", "W") == datetime(2011, 1, 31) - assert self.parser.parse("2011-W05-4T14:17:01", "WTHH:mm:ss") == datetime( - 2011, 2, 3, 14, 17, 1 - ) - assert self.parser.parse("2011W054T14:17:01", "WTHH:mm:ss") == datetime( - 2011, 2, 3, 14, 17, 1 - ) - assert self.parser.parse("2011-W05T14:17:01", "WTHH:mm:ss") == datetime( - 2011, 1, 31, 14, 17, 1 - ) - assert self.parser.parse("2011W05T141701", "WTHHmmss") == datetime( - 2011, 1, 31, 14, 17, 1 - ) - assert self.parser.parse("2011W054T141701", "WTHHmmss") == datetime( - 2011, 2, 3, 14, 17, 1 - ) - - bad_formats = [ - "201W22", - "1995-W1-4", - "2001-W34-90", - "2001--W34", - "2011-W03--3", - "thstrdjtrsrd676776r65", - "2002-W66-1T14:17:01", - "2002-W23-03T14:17:01", - ] - - for fmt in bad_formats: - with pytest.raises(ParserError): - self.parser.parse(fmt, "W") - - def test_parse_normalize_whitespace(self): - assert self.parser.parse( - "Jun 1 2005 1:33PM", "MMM D YYYY H:mmA", normalize_whitespace=True - ) == datetime(2005, 6, 1, 13, 33) - - with pytest.raises(ParserError): - self.parser.parse("Jun 1 2005 1:33PM", "MMM D YYYY H:mmA") - - assert ( - self.parser.parse( - "\t 2013-05-05 T \n 12:30:45\t123456 \t \n", - "YYYY-MM-DD T HH:mm:ss S", - normalize_whitespace=True, - ) - == datetime(2013, 5, 5, 12, 30, 45, 123456) - ) - - with pytest.raises(ParserError): - self.parser.parse( - "\t 2013-05-05 T \n 12:30:45\t123456 \t \n", - "YYYY-MM-DD T HH:mm:ss S", - ) - - assert self.parser.parse( - " \n Jun 1\t 2005\n ", "MMM D YYYY", normalize_whitespace=True - ) == datetime(2005, 6, 1) - - with pytest.raises(ParserError): - self.parser.parse(" \n Jun 1\t 2005\n ", "MMM D YYYY") - - -@pytest.mark.usefixtures("dt_parser_regex") -class TestDateTimeParserRegex: - def test_format_year(self): - - assert self.format_regex.findall("YYYY-YY") == ["YYYY", "YY"] - - def test_format_month(self): - - assert self.format_regex.findall("MMMM-MMM-MM-M") == ["MMMM", "MMM", "MM", "M"] - - def test_format_day(self): - - assert self.format_regex.findall("DDDD-DDD-DD-D") == ["DDDD", "DDD", "DD", "D"] - - def test_format_hour(self): - - assert self.format_regex.findall("HH-H-hh-h") == ["HH", "H", "hh", "h"] - - def test_format_minute(self): - - assert self.format_regex.findall("mm-m") == ["mm", "m"] - - def test_format_second(self): - - assert self.format_regex.findall("ss-s") == ["ss", "s"] - - def test_format_subsecond(self): - - assert self.format_regex.findall("SSSSSS-SSSSS-SSSS-SSS-SS-S") == [ - "SSSSSS", - "SSSSS", - "SSSS", - "SSS", - "SS", - "S", - ] - - def test_format_tz(self): - - assert self.format_regex.findall("ZZZ-ZZ-Z") == ["ZZZ", "ZZ", "Z"] - - def test_format_am_pm(self): - - assert self.format_regex.findall("A-a") == ["A", "a"] - - def test_format_timestamp(self): - - assert self.format_regex.findall("X") == ["X"] - - def test_format_timestamp_milli(self): - - assert self.format_regex.findall("x") == ["x"] - - def test_escape(self): - - escape_regex = parser.DateTimeParser._ESCAPE_RE - - assert escape_regex.findall("2018-03-09 8 [h] 40 [hello]") == ["[h]", "[hello]"] - - def test_month_names(self): - p = parser.DateTimeParser("en_us") - - text = "_".join(calendar.month_name[1:]) - - result = p._input_re_map["MMMM"].findall(text) - - assert result == calendar.month_name[1:] - - def test_month_abbreviations(self): - p = parser.DateTimeParser("en_us") - - text = "_".join(calendar.month_abbr[1:]) - - result = p._input_re_map["MMM"].findall(text) - - assert result == calendar.month_abbr[1:] - - def test_digits(self): - - assert parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall("4-56") == ["4", "56"] - assert parser.DateTimeParser._ONE_OR_TWO_OR_THREE_DIGIT_RE.findall( - "4-56-789" - ) == ["4", "56", "789"] - assert parser.DateTimeParser._ONE_OR_MORE_DIGIT_RE.findall( - "4-56-789-1234-12345" - ) == ["4", "56", "789", "1234", "12345"] - assert parser.DateTimeParser._TWO_DIGIT_RE.findall("12-3-45") == ["12", "45"] - assert parser.DateTimeParser._THREE_DIGIT_RE.findall("123-4-56") == ["123"] - assert parser.DateTimeParser._FOUR_DIGIT_RE.findall("1234-56") == ["1234"] - - def test_tz(self): - tz_z_re = parser.DateTimeParser._TZ_Z_RE - assert tz_z_re.findall("-0700") == [("-", "07", "00")] - assert tz_z_re.findall("+07") == [("+", "07", "")] - assert tz_z_re.search("15/01/2019T04:05:06.789120Z") is not None - assert tz_z_re.search("15/01/2019T04:05:06.789120") is None - - tz_zz_re = parser.DateTimeParser._TZ_ZZ_RE - assert tz_zz_re.findall("-07:00") == [("-", "07", "00")] - assert tz_zz_re.findall("+07") == [("+", "07", "")] - assert tz_zz_re.search("15/01/2019T04:05:06.789120Z") is not None - assert tz_zz_re.search("15/01/2019T04:05:06.789120") is None - - tz_name_re = parser.DateTimeParser._TZ_NAME_RE - assert tz_name_re.findall("Europe/Warsaw") == ["Europe/Warsaw"] - assert tz_name_re.findall("GMT") == ["GMT"] - - def test_timestamp(self): - timestamp_re = parser.DateTimeParser._TIMESTAMP_RE - assert timestamp_re.findall("1565707550.452729") == ["1565707550.452729"] - assert timestamp_re.findall("-1565707550.452729") == ["-1565707550.452729"] - assert timestamp_re.findall("-1565707550") == ["-1565707550"] - assert timestamp_re.findall("1565707550") == ["1565707550"] - assert timestamp_re.findall("1565707550.") == [] - assert timestamp_re.findall(".1565707550") == [] - - def test_timestamp_milli(self): - timestamp_expanded_re = parser.DateTimeParser._TIMESTAMP_EXPANDED_RE - assert timestamp_expanded_re.findall("-1565707550") == ["-1565707550"] - assert timestamp_expanded_re.findall("1565707550") == ["1565707550"] - assert timestamp_expanded_re.findall("1565707550.452729") == [] - assert timestamp_expanded_re.findall("1565707550.") == [] - assert timestamp_expanded_re.findall(".1565707550") == [] - - def test_time(self): - time_re = parser.DateTimeParser._TIME_RE - time_seperators = [":", ""] - - for sep in time_seperators: - assert time_re.findall("12") == [("12", "", "", "", "")] - assert time_re.findall("12{sep}35".format(sep=sep)) == [ - ("12", "35", "", "", "") - ] - assert time_re.findall("12{sep}35{sep}46".format(sep=sep)) == [ - ("12", "35", "46", "", "") - ] - assert time_re.findall("12{sep}35{sep}46.952313".format(sep=sep)) == [ - ("12", "35", "46", ".", "952313") - ] - assert time_re.findall("12{sep}35{sep}46,952313".format(sep=sep)) == [ - ("12", "35", "46", ",", "952313") - ] - - assert time_re.findall("12:") == [] - assert time_re.findall("12:35:46.") == [] - assert time_re.findall("12:35:46,") == [] - - -@pytest.mark.usefixtures("dt_parser") -class TestDateTimeParserISO: - def test_YYYY(self): - - assert self.parser.parse_iso("2013") == datetime(2013, 1, 1) - - def test_YYYY_DDDD(self): - assert self.parser.parse_iso("1998-136") == datetime(1998, 5, 16) - - assert self.parser.parse_iso("1998-006") == datetime(1998, 1, 6) - - with pytest.raises(ParserError): - self.parser.parse_iso("1998-456") - - # 2016 is a leap year, so Feb 29 exists (leap day) - assert self.parser.parse_iso("2016-059") == datetime(2016, 2, 28) - assert self.parser.parse_iso("2016-060") == datetime(2016, 2, 29) - assert self.parser.parse_iso("2016-061") == datetime(2016, 3, 1) - - # 2017 is not a leap year, so Feb 29 does not exist - assert self.parser.parse_iso("2017-059") == datetime(2017, 2, 28) - assert self.parser.parse_iso("2017-060") == datetime(2017, 3, 1) - assert self.parser.parse_iso("2017-061") == datetime(2017, 3, 2) - - # Since 2016 is a leap year, the 366th day falls in the same year - assert self.parser.parse_iso("2016-366") == datetime(2016, 12, 31) - - # Since 2017 is not a leap year, the 366th day falls in the next year - assert self.parser.parse_iso("2017-366") == datetime(2018, 1, 1) - - def test_YYYY_DDDD_HH_mm_ssZ(self): - - assert self.parser.parse_iso("2013-036 04:05:06+01:00") == datetime( - 2013, 2, 5, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600) - ) - - assert self.parser.parse_iso("2013-036 04:05:06Z") == datetime( - 2013, 2, 5, 4, 5, 6, tzinfo=tz.tzutc() - ) - - def test_YYYY_MM_DDDD(self): - with pytest.raises(ParserError): - self.parser.parse_iso("2014-05-125") - - def test_YYYY_MM(self): - - for separator in DateTimeParser.SEPARATORS: - assert self.parser.parse_iso(separator.join(("2013", "02"))) == datetime( - 2013, 2, 1 - ) - - def test_YYYY_MM_DD(self): - - for separator in DateTimeParser.SEPARATORS: - assert self.parser.parse_iso( - separator.join(("2013", "02", "03")) - ) == datetime(2013, 2, 3) - - def test_YYYY_MM_DDTHH_mmZ(self): - - assert self.parser.parse_iso("2013-02-03T04:05+01:00") == datetime( - 2013, 2, 3, 4, 5, tzinfo=tz.tzoffset(None, 3600) - ) - - def test_YYYY_MM_DDTHH_mm(self): - - assert self.parser.parse_iso("2013-02-03T04:05") == datetime(2013, 2, 3, 4, 5) - - def test_YYYY_MM_DDTHH(self): - - assert self.parser.parse_iso("2013-02-03T04") == datetime(2013, 2, 3, 4) - - def test_YYYY_MM_DDTHHZ(self): - - assert self.parser.parse_iso("2013-02-03T04+01:00") == datetime( - 2013, 2, 3, 4, tzinfo=tz.tzoffset(None, 3600) - ) - - def test_YYYY_MM_DDTHH_mm_ssZ(self): - - assert self.parser.parse_iso("2013-02-03T04:05:06+01:00") == datetime( - 2013, 2, 3, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600) - ) - - def test_YYYY_MM_DDTHH_mm_ss(self): - - assert self.parser.parse_iso("2013-02-03T04:05:06") == datetime( - 2013, 2, 3, 4, 5, 6 - ) - - def test_YYYY_MM_DD_HH_mmZ(self): - - assert self.parser.parse_iso("2013-02-03 04:05+01:00") == datetime( - 2013, 2, 3, 4, 5, tzinfo=tz.tzoffset(None, 3600) - ) - - def test_YYYY_MM_DD_HH_mm(self): - - assert self.parser.parse_iso("2013-02-03 04:05") == datetime(2013, 2, 3, 4, 5) - - def test_YYYY_MM_DD_HH(self): - - assert self.parser.parse_iso("2013-02-03 04") == datetime(2013, 2, 3, 4) - - def test_invalid_time(self): - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03T") - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03 044") - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03 04:05:06.") - - def test_YYYY_MM_DD_HH_mm_ssZ(self): - - assert self.parser.parse_iso("2013-02-03 04:05:06+01:00") == datetime( - 2013, 2, 3, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600) - ) - - def test_YYYY_MM_DD_HH_mm_ss(self): - - assert self.parser.parse_iso("2013-02-03 04:05:06") == datetime( - 2013, 2, 3, 4, 5, 6 - ) - - def test_YYYY_MM_DDTHH_mm_ss_S(self): - - assert self.parser.parse_iso("2013-02-03T04:05:06.7") == datetime( - 2013, 2, 3, 4, 5, 6, 700000 - ) - - assert self.parser.parse_iso("2013-02-03T04:05:06.78") == datetime( - 2013, 2, 3, 4, 5, 6, 780000 - ) - - assert self.parser.parse_iso("2013-02-03T04:05:06.789") == datetime( - 2013, 2, 3, 4, 5, 6, 789000 - ) - - assert self.parser.parse_iso("2013-02-03T04:05:06.7891") == datetime( - 2013, 2, 3, 4, 5, 6, 789100 - ) - - assert self.parser.parse_iso("2013-02-03T04:05:06.78912") == datetime( - 2013, 2, 3, 4, 5, 6, 789120 - ) - - # ISO 8601:2004(E), ISO, 2004-12-01, 4.2.2.4 ... the decimal fraction - # shall be divided from the integer part by the decimal sign specified - # in ISO 31-0, i.e. the comma [,] or full stop [.]. Of these, the comma - # is the preferred sign. - assert self.parser.parse_iso("2013-02-03T04:05:06,789123678") == datetime( - 2013, 2, 3, 4, 5, 6, 789124 - ) - - # there is no limit on the number of decimal places - assert self.parser.parse_iso("2013-02-03T04:05:06.789123678") == datetime( - 2013, 2, 3, 4, 5, 6, 789124 - ) - - def test_YYYY_MM_DDTHH_mm_ss_SZ(self): - - assert self.parser.parse_iso("2013-02-03T04:05:06.7+01:00") == datetime( - 2013, 2, 3, 4, 5, 6, 700000, tzinfo=tz.tzoffset(None, 3600) - ) - - assert self.parser.parse_iso("2013-02-03T04:05:06.78+01:00") == datetime( - 2013, 2, 3, 4, 5, 6, 780000, tzinfo=tz.tzoffset(None, 3600) - ) - - assert self.parser.parse_iso("2013-02-03T04:05:06.789+01:00") == datetime( - 2013, 2, 3, 4, 5, 6, 789000, tzinfo=tz.tzoffset(None, 3600) - ) - - assert self.parser.parse_iso("2013-02-03T04:05:06.7891+01:00") == datetime( - 2013, 2, 3, 4, 5, 6, 789100, tzinfo=tz.tzoffset(None, 3600) - ) - - assert self.parser.parse_iso("2013-02-03T04:05:06.78912+01:00") == datetime( - 2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600) - ) - - assert self.parser.parse_iso("2013-02-03 04:05:06.78912Z") == datetime( - 2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzutc() - ) - - def test_W(self): - - assert self.parser.parse_iso("2011-W05-4") == datetime(2011, 2, 3) - - assert self.parser.parse_iso("2011-W05-4T14:17:01") == datetime( - 2011, 2, 3, 14, 17, 1 - ) - - assert self.parser.parse_iso("2011W054") == datetime(2011, 2, 3) - - assert self.parser.parse_iso("2011W054T141701") == datetime( - 2011, 2, 3, 14, 17, 1 - ) - - def test_invalid_Z(self): - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912z") - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912zz") - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912Zz") - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912ZZ") - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912+Z") - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912-Z") - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912 Z") - - def test_parse_subsecond(self): - self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) - assert self.parser.parse_iso("2013-01-01 12:30:45.9") == self.expected - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) - assert self.parser.parse_iso("2013-01-01 12:30:45.98") == self.expected - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) - assert self.parser.parse_iso("2013-01-01 12:30:45.987") == self.expected - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) - assert self.parser.parse_iso("2013-01-01 12:30:45.9876") == self.expected - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) - assert self.parser.parse_iso("2013-01-01 12:30:45.98765") == self.expected - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - assert self.parser.parse_iso("2013-01-01 12:30:45.987654") == self.expected - - # use comma as subsecond separator - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - assert self.parser.parse_iso("2013-01-01 12:30:45,987654") == self.expected - - def test_gnu_date(self): - """Regression tests for parsing output from GNU date.""" - # date -Ins - assert self.parser.parse_iso("2016-11-16T09:46:30,895636557-0800") == datetime( - 2016, 11, 16, 9, 46, 30, 895636, tzinfo=tz.tzoffset(None, -3600 * 8) - ) - - # date --rfc-3339=ns - assert self.parser.parse_iso("2016-11-16 09:51:14.682141526-08:00") == datetime( - 2016, 11, 16, 9, 51, 14, 682142, tzinfo=tz.tzoffset(None, -3600 * 8) - ) - - def test_isoformat(self): - - dt = datetime.utcnow() - - assert self.parser.parse_iso(dt.isoformat()) == dt - - def test_parse_iso_normalize_whitespace(self): - assert self.parser.parse_iso( - "2013-036 \t 04:05:06Z", normalize_whitespace=True - ) == datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzutc()) - - with pytest.raises(ParserError): - self.parser.parse_iso("2013-036 \t 04:05:06Z") - - assert self.parser.parse_iso( - "\t 2013-05-05T12:30:45.123456 \t \n", normalize_whitespace=True - ) == datetime(2013, 5, 5, 12, 30, 45, 123456) - - with pytest.raises(ParserError): - self.parser.parse_iso("\t 2013-05-05T12:30:45.123456 \t \n") - - def test_parse_iso_with_leading_and_trailing_whitespace(self): - datetime_string = " 2016-11-15T06:37:19.123456" - with pytest.raises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = " 2016-11-15T06:37:19.123456 " - with pytest.raises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = "2016-11-15T06:37:19.123456 " - with pytest.raises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = "2016-11-15T 06:37:19.123456" - with pytest.raises(ParserError): - self.parser.parse_iso(datetime_string) - - # leading whitespace - datetime_string = " 2016-11-15 06:37:19.123456" - with pytest.raises(ParserError): - self.parser.parse_iso(datetime_string) - - # trailing whitespace - datetime_string = "2016-11-15 06:37:19.123456 " - with pytest.raises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = " 2016-11-15 06:37:19.123456 " - with pytest.raises(ParserError): - self.parser.parse_iso(datetime_string) - - # two dividing spaces - datetime_string = "2016-11-15 06:37:19.123456" - with pytest.raises(ParserError): - self.parser.parse_iso(datetime_string) - - def test_parse_iso_with_extra_words_at_start_and_end_invalid(self): - test_inputs = [ - "blah2016", - "blah2016blah", - "blah 2016 blah", - "blah 2016", - "2016 blah", - "blah 2016-05-16 04:05:06.789120", - "2016-05-16 04:05:06.789120 blah", - "blah 2016-05-16T04:05:06.789120", - "2016-05-16T04:05:06.789120 blah", - "2016blah", - "2016-05blah", - "2016-05-16blah", - "2016-05-16T04:05:06.789120blah", - "2016-05-16T04:05:06.789120ZblahZ", - "2016-05-16T04:05:06.789120Zblah", - "2016-05-16T04:05:06.789120blahZ", - "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", - "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", - ] - - for ti in test_inputs: - with pytest.raises(ParserError): - self.parser.parse_iso(ti) - - def test_iso8601_basic_format(self): - assert self.parser.parse_iso("20180517") == datetime(2018, 5, 17) - - assert self.parser.parse_iso("20180517T10") == datetime(2018, 5, 17, 10) - - assert self.parser.parse_iso("20180517T105513.843456") == datetime( - 2018, 5, 17, 10, 55, 13, 843456 - ) - - assert self.parser.parse_iso("20180517T105513Z") == datetime( - 2018, 5, 17, 10, 55, 13, tzinfo=tz.tzutc() - ) - - assert self.parser.parse_iso("20180517T105513.843456-0700") == datetime( - 2018, 5, 17, 10, 55, 13, 843456, tzinfo=tz.tzoffset(None, -25200) - ) - - assert self.parser.parse_iso("20180517T105513-0700") == datetime( - 2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200) - ) - - assert self.parser.parse_iso("20180517T105513-07") == datetime( - 2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200) - ) - - # ordinal in basic format: YYYYDDDD - assert self.parser.parse_iso("1998136") == datetime(1998, 5, 16) - - # timezone requires +- seperator - with pytest.raises(ParserError): - self.parser.parse_iso("20180517T1055130700") - - with pytest.raises(ParserError): - self.parser.parse_iso("20180517T10551307") - - # too many digits in date - with pytest.raises(ParserError): - self.parser.parse_iso("201860517T105513Z") - - # too many digits in time - with pytest.raises(ParserError): - self.parser.parse_iso("20180517T1055213Z") - - def test_midnight_end_day(self): - assert self.parser.parse_iso("2019-10-30T24:00:00") == datetime( - 2019, 10, 31, 0, 0, 0, 0 - ) - assert self.parser.parse_iso("2019-10-30T24:00") == datetime( - 2019, 10, 31, 0, 0, 0, 0 - ) - assert self.parser.parse_iso("2019-10-30T24:00:00.0") == datetime( - 2019, 10, 31, 0, 0, 0, 0 - ) - assert self.parser.parse_iso("2019-10-31T24:00:00") == datetime( - 2019, 11, 1, 0, 0, 0, 0 - ) - assert self.parser.parse_iso("2019-12-31T24:00:00") == datetime( - 2020, 1, 1, 0, 0, 0, 0 - ) - assert self.parser.parse_iso("2019-12-31T23:59:59.9999999") == datetime( - 2020, 1, 1, 0, 0, 0, 0 - ) - - with pytest.raises(ParserError): - self.parser.parse_iso("2019-12-31T24:01:00") - - with pytest.raises(ParserError): - self.parser.parse_iso("2019-12-31T24:00:01") - - with pytest.raises(ParserError): - self.parser.parse_iso("2019-12-31T24:00:00.1") - - with pytest.raises(ParserError): - self.parser.parse_iso("2019-12-31T24:00:00.999999") - - -@pytest.mark.usefixtures("tzinfo_parser") -class TestTzinfoParser: - def test_parse_local(self): - - assert self.parser.parse("local") == tz.tzlocal() - - def test_parse_utc(self): - - assert self.parser.parse("utc") == tz.tzutc() - assert self.parser.parse("UTC") == tz.tzutc() - - def test_parse_iso(self): - - assert self.parser.parse("01:00") == tz.tzoffset(None, 3600) - assert self.parser.parse("11:35") == tz.tzoffset(None, 11 * 3600 + 2100) - assert self.parser.parse("+01:00") == tz.tzoffset(None, 3600) - assert self.parser.parse("-01:00") == tz.tzoffset(None, -3600) - - assert self.parser.parse("0100") == tz.tzoffset(None, 3600) - assert self.parser.parse("+0100") == tz.tzoffset(None, 3600) - assert self.parser.parse("-0100") == tz.tzoffset(None, -3600) - - assert self.parser.parse("01") == tz.tzoffset(None, 3600) - assert self.parser.parse("+01") == tz.tzoffset(None, 3600) - assert self.parser.parse("-01") == tz.tzoffset(None, -3600) - - def test_parse_str(self): - - assert self.parser.parse("US/Pacific") == tz.gettz("US/Pacific") - - def test_parse_fails(self): - - with pytest.raises(parser.ParserError): - self.parser.parse("fail") - - -@pytest.mark.usefixtures("dt_parser") -class TestDateTimeParserMonthName: - def test_shortmonth_capitalized(self): - - assert self.parser.parse("2013-Jan-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) - - def test_shortmonth_allupper(self): - - assert self.parser.parse("2013-JAN-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) - - def test_shortmonth_alllower(self): - - assert self.parser.parse("2013-jan-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) - - def test_month_capitalized(self): - - assert self.parser.parse("2013-January-01", "YYYY-MMMM-DD") == datetime( - 2013, 1, 1 - ) - - def test_month_allupper(self): - - assert self.parser.parse("2013-JANUARY-01", "YYYY-MMMM-DD") == datetime( - 2013, 1, 1 - ) - - def test_month_alllower(self): - - assert self.parser.parse("2013-january-01", "YYYY-MMMM-DD") == datetime( - 2013, 1, 1 - ) - - def test_localized_month_name(self): - parser_ = parser.DateTimeParser("fr_fr") - - assert parser_.parse("2013-Janvier-01", "YYYY-MMMM-DD") == datetime(2013, 1, 1) - - def test_localized_month_abbreviation(self): - parser_ = parser.DateTimeParser("it_it") - - assert parser_.parse("2013-Gen-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) - - -@pytest.mark.usefixtures("dt_parser") -class TestDateTimeParserMeridians: - def test_meridians_lowercase(self): - assert self.parser.parse("2013-01-01 5am", "YYYY-MM-DD ha") == datetime( - 2013, 1, 1, 5 - ) - - assert self.parser.parse("2013-01-01 5pm", "YYYY-MM-DD ha") == datetime( - 2013, 1, 1, 17 - ) - - def test_meridians_capitalized(self): - assert self.parser.parse("2013-01-01 5AM", "YYYY-MM-DD hA") == datetime( - 2013, 1, 1, 5 - ) - - assert self.parser.parse("2013-01-01 5PM", "YYYY-MM-DD hA") == datetime( - 2013, 1, 1, 17 - ) - - def test_localized_meridians_lowercase(self): - parser_ = parser.DateTimeParser("hu_hu") - assert parser_.parse("2013-01-01 5 de", "YYYY-MM-DD h a") == datetime( - 2013, 1, 1, 5 - ) - - assert parser_.parse("2013-01-01 5 du", "YYYY-MM-DD h a") == datetime( - 2013, 1, 1, 17 - ) - - def test_localized_meridians_capitalized(self): - parser_ = parser.DateTimeParser("hu_hu") - assert parser_.parse("2013-01-01 5 DE", "YYYY-MM-DD h A") == datetime( - 2013, 1, 1, 5 - ) - - assert parser_.parse("2013-01-01 5 DU", "YYYY-MM-DD h A") == datetime( - 2013, 1, 1, 17 - ) - - # regression test for issue #607 - def test_es_meridians(self): - parser_ = parser.DateTimeParser("es") - - assert parser_.parse( - "Junio 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a" - ) == datetime(2019, 6, 30, 20, 0) - - with pytest.raises(ParserError): - parser_.parse( - "Junio 30, 2019 - 08:00 pasdfasdfm", "MMMM DD, YYYY - hh:mm a" - ) - - def test_fr_meridians(self): - parser_ = parser.DateTimeParser("fr") - - # the French locale always uses a 24 hour clock, so it does not support meridians - with pytest.raises(ParserError): - parser_.parse("Janvier 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a") - - -@pytest.mark.usefixtures("dt_parser") -class TestDateTimeParserMonthOrdinalDay: - def test_english(self): - parser_ = parser.DateTimeParser("en_us") - - assert parser_.parse("January 1st, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 1 - ) - assert parser_.parse("January 2nd, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 2 - ) - assert parser_.parse("January 3rd, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 3 - ) - assert parser_.parse("January 4th, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 4 - ) - assert parser_.parse("January 11th, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 11 - ) - assert parser_.parse("January 12th, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 12 - ) - assert parser_.parse("January 13th, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 13 - ) - assert parser_.parse("January 21st, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 21 - ) - assert parser_.parse("January 31st, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 31 - ) - - with pytest.raises(ParserError): - parser_.parse("January 1th, 2013", "MMMM Do, YYYY") - - with pytest.raises(ParserError): - parser_.parse("January 11st, 2013", "MMMM Do, YYYY") - - def test_italian(self): - parser_ = parser.DateTimeParser("it_it") - - assert parser_.parse("Gennaio 1º, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 1 - ) - - def test_spanish(self): - parser_ = parser.DateTimeParser("es_es") - - assert parser_.parse("Enero 1º, 2013", "MMMM Do, YYYY") == datetime(2013, 1, 1) - - def test_french(self): - parser_ = parser.DateTimeParser("fr_fr") - - assert parser_.parse("Janvier 1er, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 1 - ) - - assert parser_.parse("Janvier 2e, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 2 - ) - - assert parser_.parse("Janvier 11e, 2013", "MMMM Do, YYYY") == datetime( - 2013, 1, 11 - ) - - -@pytest.mark.usefixtures("dt_parser") -class TestDateTimeParserSearchDate: - def test_parse_search(self): - - assert self.parser.parse( - "Today is 25 of September of 2003", "DD of MMMM of YYYY" - ) == datetime(2003, 9, 25) - - def test_parse_search_with_numbers(self): - - assert self.parser.parse( - "2000 people met the 2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss" - ) == datetime(2012, 1, 1, 12, 5, 10) - - assert self.parser.parse( - "Call 01-02-03 on 79-01-01 12:05:10", "YY-MM-DD HH:mm:ss" - ) == datetime(1979, 1, 1, 12, 5, 10) - - def test_parse_search_with_names(self): - - assert self.parser.parse("June was born in May 1980", "MMMM YYYY") == datetime( - 1980, 5, 1 - ) - - def test_parse_search_locale_with_names(self): - p = parser.DateTimeParser("sv_se") - - assert p.parse("Jan föddes den 31 Dec 1980", "DD MMM YYYY") == datetime( - 1980, 12, 31 - ) - - assert p.parse("Jag föddes den 25 Augusti 1975", "DD MMMM YYYY") == datetime( - 1975, 8, 25 - ) - - def test_parse_search_fails(self): - - with pytest.raises(parser.ParserError): - self.parser.parse("Jag föddes den 25 Augusti 1975", "DD MMMM YYYY") - - def test_escape(self): - - format = "MMMM D, YYYY [at] h:mma" - assert self.parser.parse( - "Thursday, December 10, 2015 at 5:09pm", format - ) == datetime(2015, 12, 10, 17, 9) - - format = "[MMMM] M D, YYYY [at] h:mma" - assert self.parser.parse("MMMM 12 10, 2015 at 5:09pm", format) == datetime( - 2015, 12, 10, 17, 9 - ) - - format = "[It happened on] MMMM Do [in the year] YYYY [a long time ago]" - assert self.parser.parse( - "It happened on November 25th in the year 1990 a long time ago", format - ) == datetime(1990, 11, 25) - - format = "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]" - assert self.parser.parse( - "It happened on November 25th in the year 1990 a long time ago", format - ) == datetime(1990, 11, 25) - - format = "[I'm][ entirely][ escaped,][ weee!]" - assert self.parser.parse("I'm entirely escaped, weee!", format) == datetime( - 1, 1, 1 - ) - - # Special RegEx characters - format = "MMM DD, YYYY |^${}().*+?<>-& h:mm A" - assert self.parser.parse( - "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", format - ) == datetime(2017, 12, 31, 2, 0) diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_util.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/test_util.py deleted file mode 100644 index e48b4de066..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tests/test_util.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -import time -from datetime import datetime - -import pytest - -from arrow import util - - -class TestUtil: - def test_next_weekday(self): - # Get first Monday after epoch - assert util.next_weekday(datetime(1970, 1, 1), 0) == datetime(1970, 1, 5) - - # Get first Tuesday after epoch - assert util.next_weekday(datetime(1970, 1, 1), 1) == datetime(1970, 1, 6) - - # Get first Wednesday after epoch - assert util.next_weekday(datetime(1970, 1, 1), 2) == datetime(1970, 1, 7) - - # Get first Thursday after epoch - assert util.next_weekday(datetime(1970, 1, 1), 3) == datetime(1970, 1, 1) - - # Get first Friday after epoch - assert util.next_weekday(datetime(1970, 1, 1), 4) == datetime(1970, 1, 2) - - # Get first Saturday after epoch - assert util.next_weekday(datetime(1970, 1, 1), 5) == datetime(1970, 1, 3) - - # Get first Sunday after epoch - assert util.next_weekday(datetime(1970, 1, 1), 6) == datetime(1970, 1, 4) - - # Weekdays are 0-indexed - with pytest.raises(ValueError): - util.next_weekday(datetime(1970, 1, 1), 7) - - with pytest.raises(ValueError): - util.next_weekday(datetime(1970, 1, 1), -1) - - def test_total_seconds(self): - td = datetime(2019, 1, 1) - datetime(2018, 1, 1) - assert util.total_seconds(td) == td.total_seconds() - - def test_is_timestamp(self): - timestamp_float = time.time() - timestamp_int = int(timestamp_float) - - assert util.is_timestamp(timestamp_int) - assert util.is_timestamp(timestamp_float) - assert util.is_timestamp(str(timestamp_int)) - assert util.is_timestamp(str(timestamp_float)) - - assert not util.is_timestamp(True) - assert not util.is_timestamp(False) - - class InvalidTimestamp: - pass - - assert not util.is_timestamp(InvalidTimestamp()) - - full_datetime = "2019-06-23T13:12:42" - assert not util.is_timestamp(full_datetime) - - def test_normalize_timestamp(self): - timestamp = 1591161115.194556 - millisecond_timestamp = 1591161115194 - microsecond_timestamp = 1591161115194556 - - assert util.normalize_timestamp(timestamp) == timestamp - assert util.normalize_timestamp(millisecond_timestamp) == 1591161115.194 - assert util.normalize_timestamp(microsecond_timestamp) == 1591161115.194556 - - with pytest.raises(ValueError): - util.normalize_timestamp(3e17) - - def test_iso_gregorian(self): - with pytest.raises(ValueError): - util.iso_to_gregorian(2013, 0, 5) - - with pytest.raises(ValueError): - util.iso_to_gregorian(2013, 8, 0) diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tests/utils.py b/openpype/modules/ftrack/python2_vendor/arrow/tests/utils.py deleted file mode 100644 index 2a048feb3f..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tests/utils.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -import pytz -from dateutil.zoneinfo import get_zonefile_instance - -from arrow import util - - -def make_full_tz_list(): - dateutil_zones = set(get_zonefile_instance().zones) - pytz_zones = set(pytz.all_timezones) - return dateutil_zones.union(pytz_zones) - - -def assert_datetime_equality(dt1, dt2, within=10): - assert dt1.tzinfo == dt2.tzinfo - assert abs(util.total_seconds(dt1 - dt2)) < within diff --git a/openpype/modules/ftrack/python2_vendor/arrow/tox.ini b/openpype/modules/ftrack/python2_vendor/arrow/tox.ini deleted file mode 100644 index 46576b12e3..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow/tox.ini +++ /dev/null @@ -1,53 +0,0 @@ -[tox] -minversion = 3.18.0 -envlist = py{py3,27,35,36,37,38,39},lint,docs -skip_missing_interpreters = true - -[gh-actions] -python = - pypy3: pypy3 - 2.7: py27 - 3.5: py35 - 3.6: py36 - 3.7: py37 - 3.8: py38 - 3.9: py39 - -[testenv] -deps = -rrequirements.txt -allowlist_externals = pytest -commands = pytest - -[testenv:lint] -basepython = python3 -skip_install = true -deps = pre-commit -commands = - pre-commit install - pre-commit run --all-files --show-diff-on-failure - -[testenv:docs] -basepython = python3 -skip_install = true -changedir = docs -deps = - doc8 - sphinx - python-dateutil -allowlist_externals = make -commands = - doc8 index.rst ../README.rst --extension .rst --ignore D001 - make html SPHINXOPTS="-W --keep-going" - -[pytest] -addopts = -v --cov-branch --cov=arrow --cov-fail-under=100 --cov-report=term-missing --cov-report=xml -testpaths = tests - -[isort] -line_length = 88 -multi_line_output = 3 -include_trailing_comma = true - -[flake8] -per-file-ignores = arrow/__init__.py:F401 -ignore = E203,E501,W503 diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/__init__.py b/openpype/vendor/python/python_2/arrow/__init__.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/__init__.py rename to openpype/vendor/python/python_2/arrow/__init__.py diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/_version.py b/openpype/vendor/python/python_2/arrow/_version.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/_version.py rename to openpype/vendor/python/python_2/arrow/_version.py diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/api.py b/openpype/vendor/python/python_2/arrow/api.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/api.py rename to openpype/vendor/python/python_2/arrow/api.py diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/arrow.py b/openpype/vendor/python/python_2/arrow/arrow.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/arrow.py rename to openpype/vendor/python/python_2/arrow/arrow.py diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/constants.py b/openpype/vendor/python/python_2/arrow/constants.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/constants.py rename to openpype/vendor/python/python_2/arrow/constants.py diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/factory.py b/openpype/vendor/python/python_2/arrow/factory.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/factory.py rename to openpype/vendor/python/python_2/arrow/factory.py diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/formatter.py b/openpype/vendor/python/python_2/arrow/formatter.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/formatter.py rename to openpype/vendor/python/python_2/arrow/formatter.py diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/locales.py b/openpype/vendor/python/python_2/arrow/locales.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/locales.py rename to openpype/vendor/python/python_2/arrow/locales.py diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/parser.py b/openpype/vendor/python/python_2/arrow/parser.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/parser.py rename to openpype/vendor/python/python_2/arrow/parser.py diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/util.py b/openpype/vendor/python/python_2/arrow/util.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/arrow/arrow/util.py rename to openpype/vendor/python/python_2/arrow/util.py diff --git a/pyproject.toml b/pyproject.toml index 65a4b8aada..5dd67c0aae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ clique = "1.6.*" Click = "^7" dnspython = "^2.1.0" ftrack-python-api = "^2.3.3" +arrow = "^0.17" shotgun_api3 = {git = "https://github.com/shotgunsoftware/python-api.git", rev = "v3.3.3"} gazu = "^0.9.3" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) From ffcd9d995c5d8ff25ec9f3f7f223f0e55b418552 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 May 2023 15:53:21 +0200 Subject: [PATCH 226/446] moved functools and builtins from ftrack to python 2 vendor (#4993) --- openpype/modules/ftrack/ftrack_module.py | 11 +---------- .../python/python_2}/backports/__init__.py | 0 .../python_2}/backports/configparser/__init__.py | 0 .../python_2}/backports/configparser/helpers.py | 0 .../python/python_2}/backports/functools_lru_cache.py | 0 .../python/python_2}/builtins/__init__.py | 0 6 files changed, 1 insertion(+), 10 deletions(-) rename openpype/{modules/ftrack/python2_vendor/backports.functools_lru_cache => vendor/python/python_2}/backports/__init__.py (100%) rename openpype/{modules/ftrack/python2_vendor/backports.functools_lru_cache => vendor/python/python_2}/backports/configparser/__init__.py (100%) rename openpype/{modules/ftrack/python2_vendor/backports.functools_lru_cache => vendor/python/python_2}/backports/configparser/helpers.py (100%) rename openpype/{modules/ftrack/python2_vendor/backports.functools_lru_cache => vendor/python/python_2}/backports/functools_lru_cache.py (100%) rename openpype/{modules/ftrack/python2_vendor/builtins => vendor/python/python_2}/builtins/__init__.py (100%) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index bc7216d734..b5152ff9c4 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -123,16 +123,7 @@ class FtrackModule( # Add Python 2 modules python_paths = [ # `python-ftrack-api` - os.path.join(python_2_vendor, "ftrack-python-api", "source"), - # `builtins` from `python-future` - # - `python-future` is strict Python 2 module that cause crashes - # of Python 3 scripts executed through OpenPype - # (burnin script etc.) - os.path.join(python_2_vendor, "builtins"), - # `backports.functools_lru_cache` - os.path.join( - python_2_vendor, "backports.functools_lru_cache" - ) + os.path.join(python_2_vendor, "ftrack-python-api", "source") ] # Load PYTHONPATH from current launch context diff --git a/openpype/modules/ftrack/python2_vendor/backports.functools_lru_cache/backports/__init__.py b/openpype/vendor/python/python_2/backports/__init__.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/backports.functools_lru_cache/backports/__init__.py rename to openpype/vendor/python/python_2/backports/__init__.py diff --git a/openpype/modules/ftrack/python2_vendor/backports.functools_lru_cache/backports/configparser/__init__.py b/openpype/vendor/python/python_2/backports/configparser/__init__.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/backports.functools_lru_cache/backports/configparser/__init__.py rename to openpype/vendor/python/python_2/backports/configparser/__init__.py diff --git a/openpype/modules/ftrack/python2_vendor/backports.functools_lru_cache/backports/configparser/helpers.py b/openpype/vendor/python/python_2/backports/configparser/helpers.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/backports.functools_lru_cache/backports/configparser/helpers.py rename to openpype/vendor/python/python_2/backports/configparser/helpers.py diff --git a/openpype/modules/ftrack/python2_vendor/backports.functools_lru_cache/backports/functools_lru_cache.py b/openpype/vendor/python/python_2/backports/functools_lru_cache.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/backports.functools_lru_cache/backports/functools_lru_cache.py rename to openpype/vendor/python/python_2/backports/functools_lru_cache.py diff --git a/openpype/modules/ftrack/python2_vendor/builtins/builtins/__init__.py b/openpype/vendor/python/python_2/builtins/__init__.py similarity index 100% rename from openpype/modules/ftrack/python2_vendor/builtins/builtins/__init__.py rename to openpype/vendor/python/python_2/builtins/__init__.py From a627615a61b1fcb94be9d7621221bd4fedbb7452 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 May 2023 16:05:33 +0200 Subject: [PATCH 227/446] AYON: Missing files on representations (#4989) * append site information to sites and not to files * change files data to list and add them directly to representation * ignore changes of version id in hero versions * Use simpler version_id removement Co-authored-by: Roy Nieterau * filter keys in files * fix template name access --------- Co-authored-by: Roy Nieterau --- openpype/client/server/conversion_utils.py | 53 ++++++++++++---------- openpype/client/server/operations.py | 4 +- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index 1a6991329f..54b89275fe 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -125,7 +125,7 @@ def _get_default_template_name(templates): return "default" if default_template is None: - default_template = template["name"] + default_template = name return default_template @@ -664,10 +664,12 @@ def convert_v4_representation_to_v3(representation): file_info["_id"] = file_id new_files.append(file_info) - if not new_files: - new_files.append({ - "name": "studio" - }) + for file_info in new_files: + if not file_info.get("sites"): + file_info["sites"] = [{ + "name": "studio" + }] + output["files"] = new_files if representation.get("active") is False: @@ -906,20 +908,24 @@ def convert_create_representation_to_v4(representation, con): if representation.get("type") == "archived_representation": converted_representation["active"] = False - new_files = {} + new_files = [] for file_item in representation["files"]: new_file_item = { key: value for key, value in file_item.items() - if key != "_id" + if key in ("hash", "path", "size") } - file_item_id = create_entity_id() - new_files[file_item_id] = new_file_item + new_file_item.update({ + "id": create_entity_id(), + "hash_type": "op3", + "name": os.path.basename(new_file_item["path"]) + }) + new_files.append(new_file_item) + converted_representation["files"] = new_files attribs = {} data = { - "files": new_files, - "context": representation["context"] + "context": representation["context"], } representation_data = representation["data"] @@ -1221,18 +1227,19 @@ def convert_update_representation_to_v4( if "files" in update_data: new_files = update_data["files"] - if isinstance(new_files, list): - _new_files = {} - for file_item in new_files: - _file_item = { - key: value - for key, value in file_item.items() - if key != "_id" - } - file_item_id = create_entity_id() - _new_files[file_item_id] = _file_item - new_files = _new_files - new_data["files"] = new_files + if isinstance(new_files, dict): + new_files = list(new_files.values()) + + for item in new_files: + for key in tuple(item.keys()): + if key not in ("hash", "path", "size"): + item.pop(key) + item.update({ + "id": create_entity_id(), + "name": os.path.basename(item["path"]), + "hash_type": "op3", + }) + new_update_data["files"] = new_files flat_data = _to_flat_dict(new_update_data) if new_data: diff --git a/openpype/client/server/operations.py b/openpype/client/server/operations.py index 0456680737..e5254ee7b7 100644 --- a/openpype/client/server/operations.py +++ b/openpype/client/server/operations.py @@ -358,7 +358,9 @@ def prepare_hero_version_update_data(old_doc, new_doc, replace=True): Dict[str, Any]: Changes between old and new document. """ - return _prepare_update_data(old_doc, new_doc, replace) + changes = _prepare_update_data(old_doc, new_doc, replace) + changes.pop("version_id", None) + return changes def prepare_representation_update_data(old_doc, new_doc, replace=True): From c9edd211af2ee837c08c7bbff0e107117594f0ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 May 2023 14:11:21 +0200 Subject: [PATCH 228/446] replace 'fname' with 'filepath_from_context' (#5000) --- openpype/hosts/max/plugins/load/load_model.py | 2 +- openpype/hosts/max/plugins/load/load_model_fbx.py | 2 +- openpype/hosts/max/plugins/load/load_model_obj.py | 2 +- openpype/hosts/max/plugins/load/load_model_usd.py | 2 +- openpype/hosts/max/plugins/load/load_pointcloud.py | 2 +- openpype/hosts/substancepainter/plugins/load/load_mesh.py | 5 +++-- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index 58c6d3c889..cff82a593c 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -18,7 +18,7 @@ class ModelAbcLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - file_path = os.path.normpath(self.fname) + file_path = os.path.normpath(self.filepath_from_context(context)) abc_before = { c diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 663f79f9f5..12f526ab95 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -17,7 +17,7 @@ class FbxModelLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - filepath = os.path.normpath(self.fname) + filepath = os.path.normpath(self.filepath_from_context(context)) rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) rt.FBXImporterSetParam("Preserveinstances", True) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 77d4e08cfb..18a19414fa 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -18,7 +18,7 @@ class ObjLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - filepath = os.path.normpath(self.fname) + filepath = os.path.normpath(self.filepath_from_context(context)) self.log.debug("Executing command to import..") rt.Execute(f'importFile @"{filepath}" #noPrompt using:ObjImp') diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 2b34669278..48b50b9b18 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -20,7 +20,7 @@ class ModelUSDLoader(load.LoaderPlugin): from pymxs import runtime as rt # asset_filepath - filepath = os.path.normpath(self.fname) + filepath = os.path.normpath(self.filepath_from_context(context)) import_options = rt.USDImporter.CreateOptions() base_filename = os.path.basename(filepath) filename, ext = os.path.splitext(base_filename) diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index 8634e1d51f..2a1175167a 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -18,7 +18,7 @@ class PointCloudLoader(load.LoaderPlugin): """load point cloud by tyCache""" from pymxs import runtime as rt - filepath = os.path.normpath(self.fname) + filepath = os.path.normpath(self.filepath_from_context(context)) obj = rt.tyCache() obj.filename = filepath diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 822095641d..57db869a11 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -47,7 +47,8 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): if not substance_painter.project.is_open(): # Allow to 'initialize' a new project - result = prompt_new_file_with_mesh(mesh_filepath=self.fname) + path = self.filepath_from_context(context) + result = prompt_new_file_with_mesh(mesh_filepath=path) if not result: self.log.info("User cancelled new project prompt.") return @@ -65,7 +66,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): else: raise LoadError("Reload of mesh failed") - path = self.fname + path = self.filepath_from_context(context) substance_painter.project.reload_mesh(path, settings, on_mesh_reload) From 901bed8a20d977f1e97fc135da867d55025b2a04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 May 2023 09:51:22 +0200 Subject: [PATCH 229/446] General: Version attributes integration (#4991) * implemented integrator for version attributes * added data example to docstring * Use del instead of reassign Co-authored-by: Roy Nieterau * Changed log messages * made logs more explicit * grammar fix Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- .../publish/integrate_version_attrs.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 openpype/plugins/publish/integrate_version_attrs.py diff --git a/openpype/plugins/publish/integrate_version_attrs.py b/openpype/plugins/publish/integrate_version_attrs.py new file mode 100644 index 0000000000..ed179ae319 --- /dev/null +++ b/openpype/plugins/publish/integrate_version_attrs.py @@ -0,0 +1,93 @@ +import pyblish.api +import ayon_api + +from openpype import AYON_SERVER_ENABLED +from openpype.client.operations import OperationsSession + + +class IntegrateVersionAttributes(pyblish.api.ContextPlugin): + """Integrate version attributes from predefined key. + + Any integration after 'IntegrateAsset' can fill 'versionAttributes' with + attribute key & value to be updated on created version. + + The integration must make sure the attribute is available for the version + entity otherwise an error would be raised. + + Example of 'versionAttributes': + { + "ftrack_id": "0123456789-101112-131415", + "syncsketch_id": "987654321-012345-678910" + } + """ + + label = "Integrate Version Attributes" + order = pyblish.api.IntegratorOrder + 0.5 + + def process(self, context): + available_attributes = ayon_api.get_attributes_for_type("version") + skipped_attributes = set() + project_name = context.data["projectName"] + op_session = OperationsSession() + for instance in context: + label = self.get_instance_label(instance) + version_entity = instance.data.get("versionEntity") + if not version_entity: + continue + attributes = instance.data.get("versionAttributes") + if not attributes: + self.log.debug(( + "Skipping instance {} because it does not specify" + " version attributes to set." + ).format(label)) + continue + + filtered_attributes = {} + for attr, value in attributes.items(): + if attr not in available_attributes: + skipped_attributes.add(attr) + else: + filtered_attributes[attr] = value + + if not filtered_attributes: + self.log.debug(( + "Skipping instance {} because all version attributes were" + " filtered out." + ).format(label)) + continue + + self.log.debug("Updating attributes on version {} to {}".format( + version_entity["_id"], str(filtered_attributes) + )) + op_session.update_entity( + project_name, + "version", + version_entity["_id"], + {"attrib": filtered_attributes} + ) + + if skipped_attributes: + self.log.warning(( + "Skipped version attributes integration because they're" + " not available on the server: {}" + ).format(str(skipped_attributes))) + + if len(op_session): + op_session.commit() + self.log.info("Updated version attributes") + else: + self.log.debug("There are no version attributes to update") + + @staticmethod + def get_instance_label(instance): + return ( + instance.data.get("label") + or instance.data.get("name") + or instance.data.get("subset") + or str(instance) + ) + + +# Discover the plugin only in AYON mode +if not AYON_SERVER_ENABLED: + del IntegrateVersionAttributes From 5b4e474f4d028213810738fba4fe47ec7bb75632 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 May 2023 18:14:36 +0200 Subject: [PATCH 230/446] AYON: Staging versions can be used (#4992) * enhanced AddonInfo to be able get info about staging or any version * AyonDistribution is checking for use staging value * removed unused function * addons in modules manager use right versions * use right variant for settings * added temporary staging icons * fix 'is_running_staging' for ayon mode --- .../distribution/addon_distribution.py | 43 ++++++-------- common/ayon_common/distribution/addon_info.py | 54 +++++++++++++++--- common/ayon_common/resources/AYON_staging.png | Bin 0 -> 15273 bytes common/ayon_common/resources/__init__.py | 2 + openpype/lib/openpype_version.py | 4 ++ openpype/modules/base.py | 9 ++- openpype/resources/__init__.py | 7 ++- .../resources/icons/AYON_icon_staging.png | Bin 0 -> 15273 bytes .../resources/icons/AYON_splash_staging.png | Bin 0 -> 20527 bytes openpype/settings/ayon_settings.py | 5 +- 10 files changed, 86 insertions(+), 38 deletions(-) create mode 100644 common/ayon_common/resources/AYON_staging.png create mode 100644 openpype/resources/icons/AYON_icon_staging.png create mode 100644 openpype/resources/icons/AYON_splash_staging.png diff --git a/common/ayon_common/distribution/addon_distribution.py b/common/ayon_common/distribution/addon_distribution.py index dba6c20193..19aec2b031 100644 --- a/common/ayon_common/distribution/addon_distribution.py +++ b/common/ayon_common/distribution/addon_distribution.py @@ -322,22 +322,6 @@ class AyonServerDownloader(SourceDownloader): os.remove(filepath) -def get_addons_info(): - """Returns list of addon information from Server - - Returns: - List[AddonInfo]: List of metadata for addons sent from server, - parsed in AddonInfo objects - """ - - addons_info = [] - for addon in ayon_api.get_addons_info(details=True)["addons"]: - addon_info = AddonInfo.from_dict(addon) - if addon_info is not None: - addons_info.append(addon_info) - return addons_info - - def get_dependency_package(package_name=None): """Returns info about currently used dependency package. @@ -745,13 +729,16 @@ class AyonDistribution: Arguments are available for testing of the class. Args: - addon_dirpath (str): Where addons will be stored. - dependency_dirpath (str): Where dependencies will be stored. - dist_factory (DownloadFactory): Factory which cares about downloading - of items based on source type. - addons_info (List[AddonInfo]): List of prepared addons info. - dependency_package_info (Union[Dict[str, Any], None]): Dependency - package info from server. Defaults to '-1'. + addon_dirpath (Optional[str]): Where addons will be stored. + dependency_dirpath (Optional[str]): Where dependencies will be stored. + dist_factory (Optional[DownloadFactory]): Factory which cares about + downloading of items based on source type. + addons_info (Optional[List[AddonInfo]]): List of prepared addons' info. + dependency_package_info (Optional[Union[Dict[str, Any], None]]): Info + about package from server. Defaults to '-1'. + use_staging (Optional[bool]): Use staging versions of an addon. + If not passed, an environment variable 'OPENPYPE_USE_STAGING' is + checked for value '1'. """ def __init__( @@ -761,6 +748,7 @@ class AyonDistribution: dist_factory=None, addons_info=None, dependency_package_info=-1, + use_staging=None ): self._addons_dirpath = addon_dirpath or get_addons_dir() self._dependency_dirpath = dependency_dirpath or get_dependencies_dir() @@ -777,6 +765,13 @@ class AyonDistribution: self._addons_dist_items = None self._dependency_package = dependency_package_info self._dependency_dist_item = -1 + self._use_staging = use_staging + + @property + def use_staging(self): + if self._use_staging is None: + self._use_staging = os.getenv("OPENPYPE_USE_STAGING") == "1" + return self._use_staging @property def log(self): @@ -803,7 +798,7 @@ class AyonDistribution: addons_info = {} server_addons_info = ayon_api.get_addons_info(details=True) for addon in server_addons_info["addons"]: - addon_info = AddonInfo.from_dict(addon) + addon_info = AddonInfo.from_dict(addon, self.use_staging) if addon_info is None: continue addons_info[addon_info.full_name] = addon_info diff --git a/common/ayon_common/distribution/addon_info.py b/common/ayon_common/distribution/addon_info.py index 6da6f11ead..74f7b11f7f 100644 --- a/common/ayon_common/distribution/addon_info.py +++ b/common/ayon_common/distribution/addon_info.py @@ -98,19 +98,32 @@ class AddonInfo(object): authors = attr.ib(default=None) @classmethod - def from_dict(cls, data): - sources = [] - unknown_sources = [] + def from_dict_by_version(cls, data, addon_version): + """Addon info for specific version. - production_version = data.get("productionVersion") - if not production_version: + Args: + data (dict[str, Any]): Addon information from server. Should + contain information about every version under 'versions'. + addon_version (str): Addon version for which is info requested. + + Returns: + Union[AddonInfo, None]: Addon info, or None if version is not + available. + """ + + if not addon_version: return None # server payload contains info about all versions - # active addon must have 'productionVersion' and matching version info - version_data = data.get("versions", {})[production_version] + version_data = data.get("versions", {}).get(addon_version) + if not version_data: + return None + source_info = version_data.get("clientSourceInfo") require_distribution = source_info is not None + + sources = [] + unknown_sources = [] for source in (source_info or []): addon_source = convert_source(source) if addon_source is not None: @@ -119,10 +132,10 @@ class AddonInfo(object): unknown_sources.append(source) print(f"Unknown source {source.get('type')}") - full_name = "{}_{}".format(data["name"], production_version) + full_name = "{}_{}".format(data["name"], addon_version) return cls( name=data.get("name"), - version=production_version, + version=addon_version, full_name=full_name, require_distribution=require_distribution, sources=sources, @@ -134,6 +147,29 @@ class AddonInfo(object): authors=data.get("authors") ) + @classmethod + def from_dict(cls, data, use_staging=False): + """Get Addon information for production or staging version. + + Args: + data (dict[str, Any]): Addon information from server. Should + contain information about every version under 'versions'. + use_staging (bool): Use staging version if set to 'True' instead + of production. + + Returns: + Union[AddonInfo, None]: Addon info, or None if version is not + set or available. + """ + + # Active addon must have 'productionVersion' or 'stagingVersion' + # and matching version info. + if use_staging: + addon_version = data.get("stagingVersion") + else: + addon_version = data.get("productionVersion") + return cls.from_dict_by_version(data, addon_version) + @attr.s class DependencyItem(object): diff --git a/common/ayon_common/resources/AYON_staging.png b/common/ayon_common/resources/AYON_staging.png new file mode 100644 index 0000000000000000000000000000000000000000..75dadfd56c812d3bee941e90ecdd7f9f18831760 GIT binary patch literal 15273 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 Vr*$JL34HDeq4bR}Rp~lD`afSKqZR-F literal 0 HcmV?d00001 diff --git a/common/ayon_common/resources/__init__.py b/common/ayon_common/resources/__init__.py index ca61295b94..21e5fef6b2 100644 --- a/common/ayon_common/resources/__init__.py +++ b/common/ayon_common/resources/__init__.py @@ -10,6 +10,8 @@ def get_resource_path(*args): def get_icon_path(): + if os.environ.get("OPENPYPE_USE_STAGING") == "1": + return get_resource_path("AYON_staging.png") return get_resource_path("AYON.png") diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index e052002468..bdf7099f61 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -13,6 +13,7 @@ import os import sys import openpype.version +from openpype import AYON_SERVER_ENABLED from .python_module_tools import import_filepath @@ -88,6 +89,9 @@ def is_running_staging(): bool: Using staging version or not. """ + if AYON_SERVER_ENABLED: + return is_staging_enabled() + if os.environ.get("OPENPYPE_IS_STAGING") == "1": return True diff --git a/openpype/modules/base.py b/openpype/modules/base.py index c1e928ff48..ab18c15f9a 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -34,8 +34,9 @@ from openpype.settings.lib import ( from openpype.lib import ( Logger, import_filepath, - import_module_from_dirpath + import_module_from_dirpath, ) +from openpype.lib.openpype_version import is_staging_enabled from .interfaces import ( OpenPypeInterface, @@ -353,9 +354,13 @@ def _load_ayon_addons(openpype_modules, modules_key, log): )) return v3_addons_to_skip + version_key = ( + "stagingVersion" if is_staging_enabled() + else "productionVersion" + ) for addon_info in addons_info: addon_name = addon_info["name"] - addon_version = addon_info.get("productionVersion") + addon_version = addon_info.get(version_key) if not addon_version: continue diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py index 77cc0deaa2..b8671f517a 100644 --- a/openpype/resources/__init__.py +++ b/openpype/resources/__init__.py @@ -50,7 +50,7 @@ def get_openpype_production_icon_filepath(): def get_openpype_staging_icon_filepath(): filename = "openpype_icon_staging.png" if AYON_SERVER_ENABLED: - filename = "AYON_icon.png" + filename = "AYON_icon_staging.png" return get_resource("icons", filename) @@ -68,7 +68,10 @@ def get_openpype_splash_filepath(staging=None): staging = is_running_staging() if AYON_SERVER_ENABLED: - splash_file_name = "AYON_splash.png" + if staging: + splash_file_name = "AYON_splash_staging.png" + else: + splash_file_name = "AYON_splash.png" elif staging: splash_file_name = "openpype_splash_staging.png" else: diff --git a/openpype/resources/icons/AYON_icon_staging.png b/openpype/resources/icons/AYON_icon_staging.png new file mode 100644 index 0000000000000000000000000000000000000000..75dadfd56c812d3bee941e90ecdd7f9f18831760 GIT binary patch literal 15273 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 Vr*$JL34HDeq4bR}Rp~lD`afSKqZR-F literal 0 HcmV?d00001 diff --git a/openpype/resources/icons/AYON_splash_staging.png b/openpype/resources/icons/AYON_splash_staging.png new file mode 100644 index 0000000000000000000000000000000000000000..2923413664e8d14b37ee4344b928656b2bb6c0b8 GIT binary patch literal 20527 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 literal 0 HcmV?d00001 diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index ea2b72e580..00d4a27939 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -20,6 +20,7 @@ import time import six import ayon_api +from openpype.lib.openpype_version import is_staging_enabled def _convert_color(color_value): @@ -1089,7 +1090,9 @@ class AyonSettingsCache: cls._production_settings is None or cls._production_settings.is_outdated ): - value = ayon_api.get_addons_settings(only_values=False) + variant = "staging" if is_staging_enabled() else "production" + value = ayon_api.get_addons_settings( + only_values=False, variant=variant) if cls._production_settings is None: cls._production_settings = CacheItem(value) else: From bb6f43455f8db80439948919f200002dc22f7db0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 30 May 2023 11:04:02 +0200 Subject: [PATCH 231/446] fix recursive import --- openpype/settings/ayon_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 00d4a27939..ae5f5ead4d 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -20,7 +20,6 @@ import time import six import ayon_api -from openpype.lib.openpype_version import is_staging_enabled def _convert_color(color_value): @@ -1090,6 +1089,8 @@ class AyonSettingsCache: cls._production_settings is None or cls._production_settings.is_outdated ): + from openpype.lib.openpype_version import is_staging_enabled + variant = "staging" if is_staging_enabled() else "production" value = ayon_api.get_addons_settings( only_values=False, variant=variant) From a76989108ee403e56ae5ef9a891d8086a03c0cb7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 May 2023 16:23:10 +0200 Subject: [PATCH 232/446] fix key check for secure mongo connection (#5031) --- igniter/tools.py | 2 +- openpype/client/mongo/mongo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/igniter/tools.py b/igniter/tools.py index af5cbe70a9..df583fbb51 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -40,7 +40,7 @@ def should_add_certificate_path_to_mongo_url(mongo_url): add_certificate = False # Check if url 'ssl' or 'tls' are set to 'true' for key in ("ssl", "tls"): - if key in query and "true" in query["ssl"]: + if key in query and "true" in query[key]: add_certificate = True break diff --git a/openpype/client/mongo/mongo.py b/openpype/client/mongo/mongo.py index ad85782996..ce8d35fcdd 100644 --- a/openpype/client/mongo/mongo.py +++ b/openpype/client/mongo/mongo.py @@ -135,7 +135,7 @@ def should_add_certificate_path_to_mongo_url(mongo_url): add_certificate = False # Check if url 'ssl' or 'tls' are set to 'true' for key in ("ssl", "tls"): - if key in query and "true" in query["ssl"]: + if key in query and "true" in query[key]: add_certificate = True break From 594f1014ce1d2abf4410564b3f18e9281bfe83d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 May 2023 16:45:55 +0200 Subject: [PATCH 233/446] General: Qt scale enhancement (#5059) * set 'QT_SCALE_FACTOR_ROUNDING_POLICY' to 'PassThrough' * implemented 'get_openpype_qt_app' which set all openpype related attributes * implemented get app functions in igniter and ayon common * removed env varaibles 'QT_SCALE_FACTOR_ROUNDING_POLICY' * formatting fixes * fix line length * fix args --- common/ayon_common/connection/ui/lib.py | 25 +++++++++ igniter/__init__.py | 51 ++++++++++--------- .../hosts/aftereffects/api/launch_logic.py | 6 +-- openpype/hosts/aftereffects/api/pipeline.py | 6 +-- openpype/hosts/photoshop/api/pipeline.py | 6 +-- openpype/tools/context_dialog/window.py | 6 +-- openpype/tools/push_to_project/app.py | 17 +------ openpype/tools/settings/__init__.py | 7 ++- openpype/tools/tray/pype_tray.py | 35 ++----------- openpype/tools/traypublisher/window.py | 6 +-- openpype/tools/utils/__init__.py | 2 + openpype/tools/utils/lib.py | 31 +++++++++++ 12 files changed, 104 insertions(+), 94 deletions(-) diff --git a/common/ayon_common/connection/ui/lib.py b/common/ayon_common/connection/ui/lib.py index e0f0a3d6c2..a3894d0d9c 100644 --- a/common/ayon_common/connection/ui/lib.py +++ b/common/ayon_common/connection/ui/lib.py @@ -1,3 +1,7 @@ +import sys +from qtpy import QtWidgets, QtCore + + def set_style_property(widget, property_name, property_value): """Set widget's property that may affect style. @@ -9,3 +13,24 @@ def set_style_property(widget, property_name, property_value): return widget.setProperty(property_name, property_value) widget.style().polish(widget) + + +def get_qt_app(): + app = QtWidgets.QApplication.instance() + if app is not None: + return app + + for attr_name in ( + "AA_EnableHighDpiScaling", + "AA_UseHighDpiPixmaps", + ): + attr = getattr(QtCore.Qt, attr_name, None) + if attr is not None: + QtWidgets.QApplication.setAttribute(attr) + + if hasattr(QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy"): + QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( + QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough + ) + + return QtWidgets.QApplication(sys.argv) diff --git a/igniter/__init__.py b/igniter/__init__.py index aa1b1d209e..04026d5a37 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -19,21 +19,36 @@ if "OpenPypeVersion" not in sys.modules: sys.modules["OpenPypeVersion"] = OpenPypeVersion +def _get_qt_app(): + from qtpy import QtWidgets, QtCore + + app = QtWidgets.QApplication.instance() + if app is not None: + return app + + for attr_name in ( + "AA_EnableHighDpiScaling", + "AA_UseHighDpiPixmaps", + ): + attr = getattr(QtCore.Qt, attr_name, None) + if attr is not None: + QtWidgets.QApplication.setAttribute(attr) + + if hasattr(QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy"): + QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( + QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough + ) + + return app + def open_dialog(): """Show Igniter dialog.""" if os.getenv("OPENPYPE_HEADLESS_MODE"): print("!!! Can't open dialog in headless mode. Exiting.") sys.exit(1) - from qtpy import QtWidgets, QtCore from .install_dialog import InstallDialog - scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None) - if scale_attr is not None: - QtWidgets.QApplication.setAttribute(scale_attr) - - app = QtWidgets.QApplication.instance() - if not app: - app = QtWidgets.QApplication(sys.argv) + app = _get_qt_app() d = InstallDialog() d.open() @@ -47,16 +62,10 @@ def open_update_window(openpype_version): if os.getenv("OPENPYPE_HEADLESS_MODE"): print("!!! Can't open dialog in headless mode. Exiting.") sys.exit(1) - from qtpy import QtWidgets, QtCore + from .update_window import UpdateWindow - scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None) - if scale_attr is not None: - QtWidgets.QApplication.setAttribute(scale_attr) - - app = QtWidgets.QApplication.instance() - if not app: - app = QtWidgets.QApplication(sys.argv) + app = _get_qt_app() d = UpdateWindow(version=openpype_version) d.open() @@ -71,16 +80,10 @@ def show_message_dialog(title, message): if os.getenv("OPENPYPE_HEADLESS_MODE"): print("!!! Can't open dialog in headless mode. Exiting.") sys.exit(1) - from qtpy import QtWidgets, QtCore + from .message_dialog import MessageDialog - scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None) - if scale_attr is not None: - QtWidgets.QApplication.setAttribute(scale_attr) - - app = QtWidgets.QApplication.instance() - if not app: - app = QtWidgets.QApplication(sys.argv) + app = _get_qt_app() dialog = MessageDialog(title, message) dialog.open() diff --git a/openpype/hosts/aftereffects/api/launch_logic.py b/openpype/hosts/aftereffects/api/launch_logic.py index ea71122042..e90c3dc5b8 100644 --- a/openpype/hosts/aftereffects/api/launch_logic.py +++ b/openpype/hosts/aftereffects/api/launch_logic.py @@ -13,13 +13,13 @@ from wsrpc_aiohttp import ( WebSocketAsync ) -from qtpy import QtCore, QtWidgets +from qtpy import QtCore from openpype.lib import Logger -from openpype.tools.utils import host_tools from openpype.tests.lib import is_in_tests from openpype.pipeline import install_host, legacy_io from openpype.modules import ModulesManager +from openpype.tools.utils import host_tools, get_openpype_qt_app from openpype.tools.adobe_webserver.app import WebServerTool from .ws_stub import get_stub @@ -43,7 +43,7 @@ def main(*subprocess_args): install_host(host) os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" - app = QtWidgets.QApplication([]) + app = get_openpype_qt_app() app.setQuitOnLastWindowClosed(False) launcher = ProcessLauncher(subprocess_args) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 5566ca9e5b..8fc7a70dd8 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -23,6 +23,7 @@ from openpype.host import ( ILoadHost, IPublishHost ) +from openpype.tools.utils import get_openpype_qt_app from .launch_logic import get_stub from .ws_stub import ConnectionNotEstablishedYet @@ -236,10 +237,7 @@ def check_inventory(): return # Warn about outdated containers. - _app = QtWidgets.QApplication.instance() - if not _app: - print("Starting new QApplication..") - _app = QtWidgets.QApplication([]) + _app = get_openpype_qt_app() message_box = QtWidgets.QMessageBox() message_box.setIcon(QtWidgets.QMessageBox.Warning) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 88f5d63a72..56ae2a4c25 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -20,6 +20,7 @@ from openpype.host import ( from openpype.pipeline.load import any_outdated_containers from openpype.hosts.photoshop import PHOTOSHOP_HOST_DIR +from openpype.tools.utils import get_openpype_qt_app from . import lib @@ -163,10 +164,7 @@ def check_inventory(): return # Warn about outdated containers. - _app = QtWidgets.QApplication.instance() - if not _app: - print("Starting new QApplication..") - _app = QtWidgets.QApplication([]) + _app = get_openpype_qt_app() message_box = QtWidgets.QMessageBox() message_box.setIcon(QtWidgets.QMessageBox.Warning) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py index 86c53b55c5..4fe41c9949 100644 --- a/openpype/tools/context_dialog/window.py +++ b/openpype/tools/context_dialog/window.py @@ -5,7 +5,7 @@ from qtpy import QtWidgets, QtCore, QtGui from openpype import style from openpype.pipeline import AvalonMongoDB -from openpype.tools.utils.lib import center_window +from openpype.tools.utils.lib import center_window, get_openpype_qt_app from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget from openpype.tools.utils.constants import ( PROJECT_NAME_ROLE @@ -376,9 +376,7 @@ def main( strict=True ): # Run Qt application - app = QtWidgets.QApplication.instance() - if app is None: - app = QtWidgets.QApplication([]) + app = get_openpype_qt_app() window = ContextDialog() window.set_strict(strict) window.set_context(project_name, asset_name) diff --git a/openpype/tools/push_to_project/app.py b/openpype/tools/push_to_project/app.py index 9ca5fd83e9..b3ec33f353 100644 --- a/openpype/tools/push_to_project/app.py +++ b/openpype/tools/push_to_project/app.py @@ -1,6 +1,6 @@ import click -from qtpy import QtWidgets, QtCore +from openpype.tools.utils import get_openpype_qt_app from openpype.tools.push_to_project.window import PushToContextSelectWindow @@ -15,20 +15,7 @@ def main(project, version): version (str): Version id. """ - app = QtWidgets.QApplication.instance() - if not app: - # 'AA_EnableHighDpiScaling' must be set before app instance creation - high_dpi_scale_attr = getattr( - QtCore.Qt, "AA_EnableHighDpiScaling", None - ) - if high_dpi_scale_attr is not None: - QtWidgets.QApplication.setAttribute(high_dpi_scale_attr) - - app = QtWidgets.QApplication([]) - - attr = getattr(QtCore.Qt, "AA_UseHighDpiPixmaps", None) - if attr is not None: - app.setAttribute(attr) + app = get_openpype_qt_app() window = PushToContextSelectWindow() window.show() diff --git a/openpype/tools/settings/__init__.py b/openpype/tools/settings/__init__.py index a5b1ea51a5..04f64e13f1 100644 --- a/openpype/tools/settings/__init__.py +++ b/openpype/tools/settings/__init__.py @@ -1,7 +1,8 @@ import sys -from qtpy import QtWidgets, QtGui +from qtpy import QtGui from openpype import style +from openpype.tools.utils import get_openpype_qt_app from .lib import ( BTN_FIXED_SIZE, CHILD_OFFSET @@ -24,9 +25,7 @@ def main(user_role=None): user_role, ", ".join(allowed_roles) )) - app = QtWidgets.QApplication.instance() - if not app: - app = QtWidgets.QApplication(sys.argv) + app = get_openpype_qt_app() app.setWindowIcon(QtGui.QIcon(style.app_icon_path())) widget = MainWidget(user_role) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 1cf128e59d..a5876ca721 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -36,7 +36,8 @@ from openpype.settings import ( from openpype.tools.utils import ( WrappedCallbackItem, paint_image_with_color, - get_warning_pixmap + get_warning_pixmap, + get_openpype_qt_app, ) from .pype_info_widget import PypeInfoWidget @@ -858,37 +859,7 @@ class PypeTrayStarter(QtCore.QObject): def main(): - log = Logger.get_logger(__name__) - app = QtWidgets.QApplication.instance() - - high_dpi_scale_attr = None - if not app: - # 'AA_EnableHighDpiScaling' must be set before app instance creation - high_dpi_scale_attr = getattr( - QtCore.Qt, "AA_EnableHighDpiScaling", None - ) - if high_dpi_scale_attr is not None: - QtWidgets.QApplication.setAttribute(high_dpi_scale_attr) - - app = QtWidgets.QApplication([]) - - if high_dpi_scale_attr is None: - log.debug(( - "Attribute 'AA_EnableHighDpiScaling' was not set." - " UI quality may be affected." - )) - - for attr_name in ( - "AA_UseHighDpiPixmaps", - ): - attr = getattr(QtCore.Qt, attr_name, None) - if attr is None: - log.debug(( - "Missing QtCore.Qt attribute \"{}\"." - " UI quality may be affected." - ).format(attr_name)) - else: - app.setAttribute(attr) + app = get_openpype_qt_app() starter = PypeTrayStarter(app) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 3ac1b4c4ad..a1ed38dcc0 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -17,7 +17,7 @@ from openpype.pipeline import install_host from openpype.hosts.traypublisher.api import TrayPublisherHost from openpype.tools.publisher.control_qt import QtPublisherController from openpype.tools.publisher.window import PublisherWindow -from openpype.tools.utils import PlaceholderLineEdit +from openpype.tools.utils import PlaceholderLineEdit, get_openpype_qt_app from openpype.tools.utils.constants import PROJECT_NAME_ROLE from openpype.tools.utils.models import ( ProjectModel, @@ -263,9 +263,7 @@ def main(): host = TrayPublisherHost() install_host(host) - app_instance = QtWidgets.QApplication.instance() - if app_instance is None: - app_instance = QtWidgets.QApplication([]) + app_instance = get_openpype_qt_app() if platform.system().lower() == "windows": import ctypes diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 10bd527692..f35bfaee70 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -25,6 +25,7 @@ from .lib import ( set_style_property, DynamicQThread, qt_app_context, + get_openpype_qt_app, get_asset_icon, get_asset_icon_by_name, get_asset_icon_name_from_doc, @@ -68,6 +69,7 @@ __all__ = ( "set_style_property", "DynamicQThread", "qt_app_context", + "get_openpype_qt_app", "get_asset_icon", "get_asset_icon_by_name", "get_asset_icon_name_from_doc", diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 885df15da9..82ca23c848 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -14,6 +14,7 @@ from openpype.client import ( from openpype.style import ( get_default_entity_icon_color, get_objected_colors, + get_app_icon_path, ) from openpype.resources import get_image_path from openpype.lib import filter_profiles, Logger @@ -152,6 +153,36 @@ def qt_app_context(): yield app +def get_openpype_qt_app(): + """Main Qt application initialized for OpenPype processed. + + This function should be used only inside OpenPype process and never inside + other processes. + """ + + app = QtWidgets.QApplication.instance() + if app is None: + for attr_name in ( + "AA_EnableHighDpiScaling", + "AA_UseHighDpiPixmaps", + ): + attr = getattr(QtCore.Qt, attr_name, None) + if attr is not None: + QtWidgets.QApplication.setAttribute(attr) + + if hasattr( + QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy" + ): + QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( + QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough + ) + + app = QtWidgets.QApplication(sys.argv) + + app.setWindowIcon(QtGui.QIcon(get_app_icon_path())) + return app + + class SharedObjects: jobs = {} icons = {} From eed665be31771ff0bedfa830837294b31796429f Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Tue, 30 May 2023 19:51:30 +0200 Subject: [PATCH 234/446] set click to min v8 --- poetry.lock | 1548 ++++++++++++++++++++++++------------------------ pyproject.toml | 2 +- 2 files changed, 791 insertions(+), 759 deletions(-) diff --git a/poetry.lock b/poetry.lock index f96bc71fd5..b5209404df 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,106 +18,106 @@ resolved_reference = "126f7a188cfe36718f707f42ebbc597e86aa86c3" [[package]] name = "aiohttp" -version = "3.8.3" +version = "3.8.4" description = "Async http client/server framework (asyncio)" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"}, - {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"}, - {file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"}, - {file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"}, - {file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"}, - {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"}, - {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"}, - {file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"}, - {file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"}, - {file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"}, - {file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"}, - {file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"}, - {file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"}, - {file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"}, - {file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"}, - {file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"}, - {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"}, - {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"}, - {file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"}, - {file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"}, - {file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"}, - {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"}, - {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"}, - {file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"}, - {file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"}, - {file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"}, - {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea"}, + {file = "aiohttp-3.8.4-cp310-cp310-win32.whl", hash = "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1"}, + {file = "aiohttp-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24"}, + {file = "aiohttp-3.8.4-cp311-cp311-win32.whl", hash = "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d"}, + {file = "aiohttp-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc"}, + {file = "aiohttp-3.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff"}, + {file = "aiohttp-3.8.4-cp36-cp36m-win32.whl", hash = "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777"}, + {file = "aiohttp-3.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e"}, + {file = "aiohttp-3.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241"}, + {file = "aiohttp-3.8.4-cp37-cp37m-win32.whl", hash = "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a"}, + {file = "aiohttp-3.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d"}, + {file = "aiohttp-3.8.4-cp38-cp38-win32.whl", hash = "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54"}, + {file = "aiohttp-3.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4"}, + {file = "aiohttp-3.8.4-cp39-cp39-win32.whl", hash = "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a"}, + {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"}, + {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"}, ] [package.dependencies] aiosignal = ">=1.1.2" async-timeout = ">=4.0.0a3,<5.0" attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<3.0" +charset-normalizer = ">=2.0,<4.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" @@ -210,7 +210,7 @@ develop = false type = "git" url = "https://github.com/ActiveState/appdirs.git" reference = "master" -resolved_reference = "211708144ddcbba1f02e26a43efec9aef57bc9fc" +resolved_reference = "8734277956c1df3b85385e6b308e954910533884" [[package]] name = "arrow" @@ -229,19 +229,19 @@ python-dateutil = ">=2.7.0" [[package]] name = "astroid" -version = "2.13.2" +version = "2.15.5" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "astroid-2.13.2-py3-none-any.whl", hash = "sha256:8f6a8d40c4ad161d6fc419545ae4b2f275ed86d1c989c97825772120842ee0d2"}, - {file = "astroid-2.13.2.tar.gz", hash = "sha256:3bc7834720e1a24ca797fd785d77efb14f7a28ee8e635ef040b6e2d80ccb3303"}, + {file = "astroid-2.15.5-py3-none-any.whl", hash = "sha256:078e5212f9885fa85fbb0cf0101978a336190aadea6e13305409d099f71b2324"}, + {file = "astroid-2.15.5.tar.gz", hash = "sha256:1039262575027b441137ab4a62a793a9b43defb42c32d5670f38686207cd780f"}, ] [package.dependencies] lazy-object-proxy = ">=1.4.0" -typing-extensions = ">=4.0.0" +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} [[package]] @@ -269,33 +269,33 @@ files = [ [[package]] name = "attrs" -version = "22.2.0" +version = "23.1.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, ] [package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] [[package]] name = "autopep8" -version = "2.0.1" +version = "2.0.2" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "autopep8-2.0.1-py2.py3-none-any.whl", hash = "sha256:be5bc98c33515b67475420b7b1feafc8d32c1a69862498eda4983b45bffd2687"}, - {file = "autopep8-2.0.1.tar.gz", hash = "sha256:d27a8929d8dcd21c0f4b3859d2d07c6c25273727b98afc984c039df0f0d86566"}, + {file = "autopep8-2.0.2-py2.py3-none-any.whl", hash = "sha256:86e9303b5e5c8160872b2f5ef611161b2893e9bfe8ccc7e2f76385947d57a2f1"}, + {file = "autopep8-2.0.2.tar.gz", hash = "sha256:f9849cdd62108cb739dbcdbfb7fdcc9a30d1b63c4cc3e1c1f893b5360941b61c"}, ] [package.dependencies] @@ -304,19 +304,16 @@ tomli = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "babel" -version = "2.11.0" +version = "2.12.1" description = "Internationalization utilities" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, - {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, ] -[package.dependencies] -pytz = ">=2015.7" - [[package]] name = "bcrypt" version = "4.0.1" @@ -371,14 +368,14 @@ test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "py [[package]] name = "blessed" -version = "1.19.1" +version = "1.20.0" description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." category = "main" optional = false python-versions = ">=2.7" files = [ - {file = "blessed-1.19.1-py2.py3-none-any.whl", hash = "sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b"}, - {file = "blessed-1.19.1.tar.gz", hash = "sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc"}, + {file = "blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058"}, + {file = "blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"}, ] [package.dependencies] @@ -388,26 +385,26 @@ wcwidth = ">=0.1.4" [[package]] name = "cachetools" -version = "5.2.1" +version = "5.3.1" description = "Extensible memoizing collections and decorators" category = "main" optional = false -python-versions = "~=3.7" +python-versions = ">=3.7" files = [ - {file = "cachetools-5.2.1-py3-none-any.whl", hash = "sha256:8462eebf3a6c15d25430a8c27c56ac61340b2ecf60c9ce57afc2b97e450e47da"}, - {file = "cachetools-5.2.1.tar.gz", hash = "sha256:5991bc0e08a1319bb618d3195ca5b6bc76646a49c21d55962977197b301cc1fe"}, + {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, + {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, ] [[package]] name = "certifi" -version = "2022.12.7" +version = "2023.5.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, ] [[package]] @@ -501,31 +498,104 @@ files = [ [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, ] -[package.extras] -unicode-backport = ["unicodedata2"] - [[package]] name = "click" -version = "7.1.2" +version = "8.1.3" description = "Composable command line interface toolkit" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" files = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "clique" version = "1.6.1" @@ -547,7 +617,7 @@ test = ["pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -584,63 +654,72 @@ files = [ [[package]] name = "coverage" -version = "7.0.5" +version = "7.2.7" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b"}, - {file = "coverage-7.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809"}, - {file = "coverage-7.0.5-cp310-cp310-win32.whl", hash = "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21"}, - {file = "coverage-7.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad"}, - {file = "coverage-7.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca"}, - {file = "coverage-7.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095"}, - {file = "coverage-7.0.5-cp311-cp311-win32.whl", hash = "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831"}, - {file = "coverage-7.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea"}, - {file = "coverage-7.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47"}, - {file = "coverage-7.0.5-cp37-cp37m-win32.whl", hash = "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882"}, - {file = "coverage-7.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d"}, - {file = "coverage-7.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb"}, - {file = "coverage-7.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499"}, - {file = "coverage-7.0.5-cp38-cp38-win32.whl", hash = "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16"}, - {file = "coverage-7.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af"}, - {file = "coverage-7.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab"}, - {file = "coverage-7.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1"}, - {file = "coverage-7.0.5-cp39-cp39-win32.whl", hash = "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904"}, - {file = "coverage-7.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f"}, - {file = "coverage-7.0.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0"}, - {file = "coverage-7.0.5.tar.gz", hash = "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, ] [package.dependencies] @@ -651,47 +730,45 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "39.0.0" +version = "40.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"}, - {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"}, - {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"}, - {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"}, - {file = "cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"}, - {file = "cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"}, - {file = "cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"}, + {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b"}, + {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b"}, + {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"}, + {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c"}, + {file = "cryptography-40.0.2-cp36-abi3-win32.whl", hash = "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9"}, + {file = "cryptography-40.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404"}, + {file = "cryptography-40.0.2.tar.gz", hash = "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99"}, ] [package.dependencies] cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "ruff"] +pep8test = ["black", "check-manifest", "mypy", "ruff"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] +test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] +tox = ["tox"] [[package]] name = "cx-freeze" @@ -862,14 +939,14 @@ stone = ">=2" [[package]] name = "enlighten" -version = "1.11.1" +version = "1.11.2" description = "Enlighten Progress Bar" category = "main" optional = false python-versions = "*" files = [ - {file = "enlighten-1.11.1-py2.py3-none-any.whl", hash = "sha256:e825eb534ca80778bb7d46e5581527b2a6fae559b6cf09e290a7952c6e11961e"}, - {file = "enlighten-1.11.1.tar.gz", hash = "sha256:57abd98a3d3f83484ef9f91f9255f4d23c8b3097ecdb647c7b9b0049d600b7f8"}, + {file = "enlighten-1.11.2-py2.py3-none-any.whl", hash = "sha256:98c9eb20e022b6a57f1c8d4f17e16760780b6881e6d658c40f52d21255ea45f3"}, + {file = "enlighten-1.11.2.tar.gz", hash = "sha256:9284861dee5a272e0e1a3758cd3f3b7180b1bd1754875da76876f2a7f46ccb61"}, ] [package.dependencies] @@ -878,30 +955,30 @@ prefixed = ">=0.3.2" [[package]] name = "evdev" -version = "1.6.0" +version = "1.6.1" description = "Bindings to the Linux input handling subsystem" category = "main" optional = false python-versions = "*" files = [ - {file = "evdev-1.6.0.tar.gz", hash = "sha256:ecfa01b5c84f7e8c6ced3367ac95288f43cd84efbfd7dd7d0cdbfc0d18c87a6a"}, + {file = "evdev-1.6.1.tar.gz", hash = "sha256:299db8628cc73b237fc1cc57d3c2948faa0756e2a58b6194b5bf81dc2081f1e3"}, ] [[package]] name = "filelock" -version = "3.9.0" +version = "3.12.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, - {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, + {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"}, + {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"}, ] [package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -1006,14 +1083,14 @@ files = [ [[package]] name = "ftrack-python-api" -version = "2.3.3" +version = "2.5.0" description = "Python API for ftrack." category = "main" optional = false -python-versions = ">=2.7.9, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, < 3.10" +python-versions = ">=2.7.9, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "ftrack-python-api-2.3.3.tar.gz", hash = "sha256:358f37e5b1c5635eab107c19e27a0c890d512877f78af35b1ac416e90c037295"}, - {file = "ftrack_python_api-2.3.3-py2.py3-none-any.whl", hash = "sha256:82834c4d5def5557a2ea547a7e6f6ba84d3129e8f90457d8bbd85b287a2c39f6"}, + {file = "ftrack-python-api-2.5.0.tar.gz", hash = "sha256:95205022552b1abadec5e9dcb225762b8e8b9f16ebeadba374e56c25e69e6954"}, + {file = "ftrack_python_api-2.5.0-py2.py3-none-any.whl", hash = "sha256:59ef3f1d47e5c1df8c3f7ebcc937bbc9a5613b147f9ed083f10cff6370f0750d"}, ] [package.dependencies] @@ -1075,14 +1152,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.30" -description = "GitPython is a python library used to interact with Git repositories" +version = "3.1.31" +description = "GitPython is a Python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"}, - {file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"}, + {file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"}, + {file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"}, ] [package.dependencies] @@ -1133,14 +1210,14 @@ uritemplate = ">=3.0.0,<4dev" [[package]] name = "google-auth" -version = "2.16.0" +version = "2.17.3" description = "Google Authentication Library" category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ - {file = "google-auth-2.16.0.tar.gz", hash = "sha256:ed7057a101af1146f0554a769930ac9de506aeca4fd5af6543ebe791851a9fbd"}, - {file = "google_auth-2.16.0-py2.py3-none-any.whl", hash = "sha256:5045648c821fb72384cdc0e82cc326df195f113a33049d9b62b74589243d2acc"}, + {file = "google-auth-2.17.3.tar.gz", hash = "sha256:ce311e2bc58b130fddf316df57c9b3943c2a7b4f6ec31de9663a9333e4064efc"}, + {file = "google_auth-2.17.3-py2.py3-none-any.whl", hash = "sha256:f586b274d3eb7bd932ea424b1c702a30e0393a2e2bc4ca3eae8263ffd8be229f"}, ] [package.dependencies] @@ -1175,14 +1252,14 @@ six = "*" [[package]] name = "googleapis-common-protos" -version = "1.58.0" +version = "1.59.0" description = "Common protobufs used in Google APIs" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.58.0.tar.gz", hash = "sha256:c727251ec025947d545184ba17e3578840fc3a24a0516a020479edab660457df"}, - {file = "googleapis_common_protos-1.58.0-py2.py3-none-any.whl", hash = "sha256:ca3befcd4580dab6ad49356b46bf165bb68ff4b32389f028f1abd7c10ab9519a"}, + {file = "googleapis-common-protos-1.59.0.tar.gz", hash = "sha256:4168fcb568a826a52f23510412da405abd93f4d23ba544bb68d943b14ba3cb44"}, + {file = "googleapis_common_protos-1.59.0-py2.py3-none-any.whl", hash = "sha256:b287dc48449d1d41af0c69f4ea26242b5ae4c3d7249a38b0984c86a4caffff1f"}, ] [package.dependencies] @@ -1193,14 +1270,14 @@ grpc = ["grpcio (>=1.44.0,<2.0.0dev)"] [[package]] name = "httplib2" -version = "0.21.0" +version = "0.22.0" description = "A comprehensive HTTP client library." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "httplib2-0.21.0-py3-none-any.whl", hash = "sha256:987c8bb3eb82d3fa60c68699510a692aa2ad9c4bd4f123e51dfb1488c14cdd01"}, - {file = "httplib2-0.21.0.tar.gz", hash = "sha256:fc144f091c7286b82bec71bdbd9b27323ba709cc612568d3000893bfd9cb4b34"}, + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, ] [package.dependencies] @@ -1208,14 +1285,14 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 [[package]] name = "identify" -version = "2.5.13" +version = "2.5.24" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "identify-2.5.13-py2.py3-none-any.whl", hash = "sha256:8aa48ce56e38c28b6faa9f261075dea0a942dfbb42b341b4e711896cbb40f3f7"}, - {file = "identify-2.5.13.tar.gz", hash = "sha256:abb546bca6f470228785338a01b539de8a85bbf46491250ae03363956d8ebb10"}, + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, ] [package.extras] @@ -1247,14 +1324,14 @@ files = [ [[package]] name = "importlib-metadata" -version = "6.0.0" +version = "6.6.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, - {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, + {file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"}, + {file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"}, ] [package.dependencies] @@ -1279,19 +1356,19 @@ files = [ [[package]] name = "isort" -version = "5.11.4" +version = "5.12.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, - {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, ] [package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile-deprecated-finder = ["pipreqs", "requirementslib"] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] @@ -1447,58 +1524,47 @@ files = [ [[package]] name = "lief" -version = "0.12.3" +version = "0.13.1" description = "Library to instrument executable formats" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "lief-0.12.3-cp310-cp310-macosx_10_14_arm64.whl", hash = "sha256:66724f337e6a36cea1a9380f13b59923f276c49ca837becae2e7be93a2e245d9"}, - {file = "lief-0.12.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6d18aafa2028587c98f6d4387bec94346e92f2b5a8a5002f70b1cf35b1c045cc"}, - {file = "lief-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f69d125caaa8d5ddb574f29cc83101e165ebea1a9f18ad042eb3544081a797"}, - {file = "lief-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c078d6230279ffd3bca717c79664fb8368666f610b577deb24b374607936e9c1"}, - {file = "lief-0.12.3-cp310-cp310-win32.whl", hash = "sha256:e3a6af926532d0aac9e7501946134513d63217bacba666e6f7f5a0b7e15ba236"}, - {file = "lief-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:0750b72e3aa161e1fb0e2e7f571121ae05d2428aafd742ff05a7656ad2288447"}, - {file = "lief-0.12.3-cp311-cp311-macosx_10_14_arm64.whl", hash = "sha256:b5c123cb99a7879d754c059e299198b34e7e30e3b64cf22e8962013db0099f47"}, - {file = "lief-0.12.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8bc58fa26a830df6178e36f112cb2bbdd65deff593f066d2d51434ff78386ba5"}, - {file = "lief-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74ac6143ac6ccd813c9b068d9c5f1f9d55c8813c8b407387eb57de01c3db2d74"}, - {file = "lief-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04eb6b70d646fb5bd6183575928ee23715550f161f2832cbcd8c6ff2071fb408"}, - {file = "lief-0.12.3-cp311-cp311-win32.whl", hash = "sha256:7e2d0a53c403769b04adcf8df92e83c5e25f9103a052aa7f17b0a9cf057735fb"}, - {file = "lief-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:7f6395c12ee1bc4a5162f567cba96d0c72dfb660e7902e84d4f3029daf14fe33"}, - {file = "lief-0.12.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:71327fdc764fd2b1f3cd371d8ac5e0b801bde32b71cfcf7dccee506d46768539"}, - {file = "lief-0.12.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d320fb80ed5b42b354b8e4f251ab05a51929c162c57c377b5e95ad4b1c1b415d"}, - {file = "lief-0.12.3-cp36-cp36m-win32.whl", hash = "sha256:176fa6c342dd480195cda34a20f62ac76dfae103b22ca7583b762e0b434ee1f3"}, - {file = "lief-0.12.3-cp36-cp36m-win_amd64.whl", hash = "sha256:3a18fe108fb82a2640864deef933731afe77413b1226551796ef2c373a1b3a2a"}, - {file = "lief-0.12.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:c73e990cd2737d1060b8c1e8edcc128832806995b69d1d6bf191409e2cea7bde"}, - {file = "lief-0.12.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:5fa2b1c8ffe47ee66b2507c2bb4e3fd628965532b7888c0627d10e690b5ef20c"}, - {file = "lief-0.12.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f224e9a261e88099f86160f121d088d30894c2946e3e551cf11c678daadcf2b"}, - {file = "lief-0.12.3-cp37-cp37m-win32.whl", hash = "sha256:3481d7c9fb3d3a1acff53851f40efd1a5a05d354312d367294bc2e310b736826"}, - {file = "lief-0.12.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4e5173e1be5ebf43594f4eb187cbcb04758761942bc0a1e685ea1cb9047dc0d9"}, - {file = "lief-0.12.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54d6a45e01260b9c8bf1c99f58257cff5338aee5c02eacfeee789f9d15cf38c6"}, - {file = "lief-0.12.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:4501dc399fb15dc7a3c8df4a76264a86be6d581d99098dafc3a67626149d8ff1"}, - {file = "lief-0.12.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c848aadac0816268aeb9dde7cefdb54bf24f78e664a19e97e74c92d3be1bb147"}, - {file = "lief-0.12.3-cp38-cp38-win32.whl", hash = "sha256:d7e35f9ee9dd6e79add3b343f83659b71c05189e5cb224e02a1902ddc7654e96"}, - {file = "lief-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:b00667257b43e93d94166c959055b6147d46d302598f3ee55c194b40414c89cc"}, - {file = "lief-0.12.3-cp39-cp39-macosx_10_14_arm64.whl", hash = "sha256:e6a1b5b389090d524621c2455795e1262f62dc9381bedd96f0cd72b878c4066d"}, - {file = "lief-0.12.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ae773196df814202c0c51056163a1478941b299512b09660a3c37be3c7fac81e"}, - {file = "lief-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:66ddf88917ec7b00752687c476bb2771dc8ec19bd7e4c0dcff1f8ef774cad4e9"}, - {file = "lief-0.12.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4a47f410032c63ac3be051d963d0337d6b47f0e94bfe8e946ab4b6c428f4d0f8"}, - {file = "lief-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbd11367c2259bd1131a6c8755dcde33314324de5ea029227bfbc7d3755871e6"}, - {file = "lief-0.12.3-cp39-cp39-win32.whl", hash = "sha256:2ce53e311918c3e5b54c815ef420a747208d2a88200c41cd476f3dd1eb876bcf"}, - {file = "lief-0.12.3-cp39-cp39-win_amd64.whl", hash = "sha256:446e53ccf0ebd1616c5d573470662ff71ca6df3cd62ec1764e303764f3f03cca"}, - {file = "lief-0.12.3.zip", hash = "sha256:62e81d2f1a827d43152aed12446a604627e8833493a51dca027026eed0ce7128"}, + {file = "lief-0.13.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:b53317d78f8b7528e3f2f358b3f9334a1a84fae88c5aec1a3b7717ed31bfb066"}, + {file = "lief-0.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb8b285a6c670df590c36fc0c19b9d2e32b99f17e57afa29bb3052f1d55aa50f"}, + {file = "lief-0.13.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:be871116faa698b6d9da76b0caec2ec5b7e7b8781cfb3a4ac0c4e348fb37ab49"}, + {file = "lief-0.13.1-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:c6839df875e912edd3fc553ab5d1b916527adee9c57ba85c69314a93f7ba2e15"}, + {file = "lief-0.13.1-cp310-cp310-win32.whl", hash = "sha256:b1f295dbb57094443926ac6051bee9a1945d92344f470da1cb506060eb2f91ac"}, + {file = "lief-0.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:8439805a389cc67b6d4ea7d757a3211f22298edce53c5b064fdf8bf05fabba54"}, + {file = "lief-0.13.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:3cfbc6c50f9e3a8015cd5ee88dfe83f423562c025439143bbd5c086a3f9fe599"}, + {file = "lief-0.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:661abaa48bc032b9a7529e0b73d2ced3e4a1f13381592f6b9e940750b07a5ac2"}, + {file = "lief-0.13.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:23617d96d162081f8bf315d9b0494845891f8d0f04ad60991b83367ee9e261aa"}, + {file = "lief-0.13.1-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:aa7f45c5125be80a513624d3a5f6bd50751c2edc6de5357fde218580111c8535"}, + {file = "lief-0.13.1-cp311-cp311-win32.whl", hash = "sha256:018b542f09fe2305e1585a3e63a7e5132927b835062b456e5c8c571db7784d1e"}, + {file = "lief-0.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:bfbf8885a3643ea9aaf663d039f50ca58b228886c3fe412725b22851aeda3b77"}, + {file = "lief-0.13.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a0472636ab15b9afecf8b5d55966912af8cb4de2f05b98fc05c87d51880d0208"}, + {file = "lief-0.13.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ccfba33c02f21d4ede26ab85eb6539a00e74e236569c13dcbab2e157b73673c4"}, + {file = "lief-0.13.1-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:e414d6c23f26053f4824d080885ab1b75482122796cba7d09cbf157900646289"}, + {file = "lief-0.13.1-cp38-cp38-win32.whl", hash = "sha256:a18fee5cf69adf9d5ee977778ccd46c39c450960f806231b26b69011f81bc712"}, + {file = "lief-0.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:04c87039d1e68ebc467f83136179626403547dd1ce851541345f8ca0b1fe6c5b"}, + {file = "lief-0.13.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:0283a4c749afe58be8e21cdd9be79c657c51ca9b8346f75f4b97349b1f022851"}, + {file = "lief-0.13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95a4b6d1f8dba9360aecf7542e54ce5eb02c0e88f2d827b5445594d5d51109f5"}, + {file = "lief-0.13.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:16753bd72b1e3932d94d088a93b64e08c1f6c8bce1b064b47fe66ed73d9562b2"}, + {file = "lief-0.13.1-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:965fadb1301d1a81f16067e4fa743d2be3f6aa71391a83b752ff811ec74b0766"}, + {file = "lief-0.13.1-cp39-cp39-win32.whl", hash = "sha256:57bdb0471760c4ff520f5e5d005e503cc7ea3ebe22df307bb579a1a561b8c4e9"}, + {file = "lief-0.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:a3c900f49c3d3135c728faeb386d13310bb3511eb2d4e1c9b109b48ae2658361"}, ] [[package]] name = "linkify-it-py" -version = "2.0.0" +version = "2.0.2" description = "Links recognition library with FULL unicode support." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "linkify-it-py-2.0.0.tar.gz", hash = "sha256:476464480906bed8b2fa3813bf55566282e55214ad7e41b7d1c2b564666caf2f"}, - {file = "linkify_it_py-2.0.0-py3-none-any.whl", hash = "sha256:1bff43823e24e507a099e328fc54696124423dd6320c75a9da45b4b754b748ad"}, + {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, + {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, ] [package.dependencies] @@ -1506,7 +1572,7 @@ uc-micro-py = "*" [package.extras] benchmark = ["pytest", "pytest-benchmark"] -dev = ["black", "flake8", "isort", "pre-commit"] +dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] doc = ["myst-parser", "sphinx", "sphinx-book-theme"] test = ["coverage", "pytest", "pytest-cov"] @@ -1542,24 +1608,24 @@ mistune = "0.8.4" [[package]] name = "markdown-it-py" -version = "2.1.0" +version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, - {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] -code-style = ["pre-commit (==2.6)"] -compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] @@ -1658,14 +1724,14 @@ files = [ [[package]] name = "mdit-py-plugins" -version = "0.3.3" +version = "0.3.5" description = "Collection of plugins for markdown-it-py" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mdit-py-plugins-0.3.3.tar.gz", hash = "sha256:5cfd7e7ac582a594e23ba6546a2f406e94e42eb33ae596d0734781261c251260"}, - {file = "mdit_py_plugins-0.3.3-py3-none-any.whl", hash = "sha256:36d08a29def19ec43acdcd8ba471d3ebab132e7879d442760d963f19913e04b9"}, + {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, + {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, ] [package.dependencies] @@ -1813,54 +1879,19 @@ testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", [[package]] name = "nodeenv" -version = "1.7.0" +version = "1.8.0" description = "Node.js virtual environment builder" category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, ] [package.dependencies] setuptools = "*" -[[package]] -name = "opencolorio" -version = "2.2.1" -description = "OpenColorIO (OCIO) is a complete color management solution geared towards motion picture production with an emphasis on visual effects and computer animation." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "opencolorio-2.2.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a9feec76e450325f12203264194d905a938d5e7944772b806886f9531e406d42"}, - {file = "opencolorio-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7eeae01328b359408940a1f29d53b15b034755413d95d08781b76084ee14cbb1"}, - {file = "opencolorio-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85b63a9162e99f0f29ef4074017d1b6e8caf59096043fb91cbacfc5bc01fa0b9"}, - {file = "opencolorio-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67d19ea54daff2b209b91981da415aa41ea8e3a60fecd5dd843ae13272d38dcf"}, - {file = "opencolorio-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da0043a1007d269b5da3c8ca1de8c63926b38bf5e08cfade6cb8f2f5f6b663b9"}, - {file = "opencolorio-2.2.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:62180cec075cae8dff56eeb977132eb9755d7fe312d8d34236cba838cb9314b3"}, - {file = "opencolorio-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24b7bfc4b77c04845de847373e58232c48838042d5e45e027b8bf64bada988e3"}, - {file = "opencolorio-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41cadab13b18dbedd992df2056c787cf38bf89a5b0903b90f701d5228ac496f9"}, - {file = "opencolorio-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa278dd4414791a5605e685b562b6ad1c729a4a44c1c906151f5bca10c0ff10e"}, - {file = "opencolorio-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:7b44858c26b662ec42b089f8f85ea3aa63aa04e0e58e902a4cbf8cae0fbd4c6c"}, - {file = "opencolorio-2.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:07fce0d36a6041b524b2122b9f55fbd03e029def5a22e93822041b652b60590a"}, - {file = "opencolorio-2.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae043bc588d9ee98f54fe9524481eba5634d6dd70d0c70e1bd242b60a3a81731"}, - {file = "opencolorio-2.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a4ad1a4ed5742a7dda41f0548274e8328b2774ce04dfc31fd5dfbacabc4c166"}, - {file = "opencolorio-2.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bd885e34898c204db19a9e6926c551a74bda6d8e7d3ef27596630e3422b99b1"}, - {file = "opencolorio-2.2.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:86ed205bec96fd84e882d431c181560df0cf6f0f73150976303b6f3ff1d9d5ed"}, - {file = "opencolorio-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c1bf1c19baa86203e2329194ea837161520dae5c94e4f04b7659e9bfe4f1a6a9"}, - {file = "opencolorio-2.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639f7052da7086d999c0d84e424453fb44abc8f2d22ec8601d20d8ee9d90384b"}, - {file = "opencolorio-2.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7e3208c5c1ac63a6e921398db661fdd9309b17253b285f227818713f3faec92"}, - {file = "opencolorio-2.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:68814600c0d8c07b552e1f1e4e32d45bffba4cb49b41481e5d4dd0bc56a206ea"}, - {file = "opencolorio-2.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:cb5337ac2804dbb90c856b423d2799d3dc35f9c948da25d8e6506d1dd8200df7"}, - {file = "opencolorio-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:425593a96de7927aa7cda065dc3729e881de1d0b72c43e704e02962adb63b4ad"}, - {file = "opencolorio-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8be9f6af01e4c710de4cc03c9b6de04ef0844bf611e9100abf045ec62a4c685a"}, - {file = "opencolorio-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84788002aa28151409f2367a040e9d39ffea0a9129777451bd0c55ac87d9d47"}, - {file = "opencolorio-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:d92802922bc4e2ff3e9a06d44b6055efd1863abb1aaf0243849d35b077b72253"}, - {file = "opencolorio-2.2.1.tar.gz", hash = "sha256:283abb8db5bc18ab9686e08255a9245deaba3d7837be5363b7a69b0902b73a78"}, -] - [[package]] name = "opentimelineio" version = "0.14.1" @@ -1903,39 +1934,37 @@ view = ["PySide2 (>=5.11,<6.0)"] [[package]] name = "packaging" -version = "23.0" +version = "23.1" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] [[package]] name = "paramiko" -version = "2.12.0" +version = "3.2.0" description = "SSH2 protocol library" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "paramiko-2.12.0-py2.py3-none-any.whl", hash = "sha256:b2df1a6325f6996ef55a8789d0462f5b502ea83b3c990cbb5bbe57345c6812c4"}, - {file = "paramiko-2.12.0.tar.gz", hash = "sha256:376885c05c5d6aa6e1f4608aac2a6b5b0548b1add40274477324605903d9cd49"}, + {file = "paramiko-3.2.0-py3-none-any.whl", hash = "sha256:df0f9dd8903bc50f2e10580af687f3015bf592a377cd438d2ec9546467a14eb8"}, + {file = "paramiko-3.2.0.tar.gz", hash = "sha256:93cdce625a8a1dc12204439d45033f3261bdb2c201648cfcdc06f9fd0f94ec29"}, ] [package.dependencies] -bcrypt = ">=3.1.3" -cryptography = ">=2.5" -pynacl = ">=1.0.1" -six = "*" +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" [package.extras] -all = ["bcrypt (>=3.1.3)", "gssapi (>=1.4.1)", "invoke (>=1.3)", "pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "pywin32 (>=2.1.8)"] -ed25519 = ["bcrypt (>=3.1.3)", "pynacl (>=1.0.1)"] +all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -invoke = ["invoke (>=1.3)"] +invoke = ["invoke (>=2.0)"] [[package]] name = "parso" @@ -1955,18 +1984,19 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "patchelf" -version = "0.17.2.0" +version = "0.17.2.1" description = "A small utility to modify the dynamic linker and RPATH of ELF executables." category = "dev" optional = false python-versions = "*" files = [ - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b8d86f32e1414d6964d5d166ddd2cf829d156fba0d28d32a3bd0192f987f4529"}, - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:9233a0f2fc73820c5bd468f27507bdf0c9ac543f07c7f9888bb7cf910b1be22f"}, - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:6601d7d831508bcdd3d8ebfa6435c2379bf11e41af2409ae7b88de572926841c"}, - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.musllinux_1_1_i686.whl", hash = "sha256:c62a34f0c25e6c2d6ae44389f819a00ccdf3f292ad1b814fbe1cc23cb27023ce"}, - {file = "patchelf-0.17.2.0-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:1b9fd14f300341dc020ae05c49274dd1fa6727eabb4e61dd7fb6fb3600acd26e"}, - {file = "patchelf-0.17.2.0.tar.gz", hash = "sha256:dedf987a83d7f6d6f5512269e57f5feeec36719bd59567173b6d9beabe019efe"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:fc329da0e8f628bd836dfb8eaf523547e342351fa8f739bf2b3fe4a6db5a297c"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ccb266a94edf016efe80151172c26cff8c2ec120a57a1665d257b0442784195d"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:f47b5bdd6885cfb20abdd14c707d26eb6f499a7f52e911865548d4aa43385502"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:a9e6ebb0874a11f7ed56d2380bfaa95f00612b23b15f896583da30c2059fcfa8"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.musllinux_1_1_i686.whl", hash = "sha256:3c8d58f0e4c1929b1c7c45ba8da5a84a8f1aa6a82a46e1cfb2e44a4d40f350e5"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d1a9bc0d4fd80c038523ebdc451a1cce75237cfcc52dbd1aca224578001d5927"}, + {file = "patchelf-0.17.2.1.tar.gz", hash = "sha256:a6eb0dd452ce4127d0d5e1eb26515e39186fa609364274bc1b0b77539cfa7031"}, ] [package.extras] @@ -1989,110 +2019,99 @@ six = "*" [[package]] name = "pillow" -version = "9.4.0" +version = "9.5.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, - {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, - {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, - {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, - {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, - {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, - {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, - {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, - {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, - {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, - {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, - {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, - {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, - {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, - {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, - {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, - {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, - {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, - {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, - {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, - {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, - {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, - {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, - {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, - {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "platformdirs" -version = "2.6.2" +version = "3.5.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, - {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, + {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"}, + {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"}, ] [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -2139,14 +2158,14 @@ six = ">=1.5.2" [[package]] name = "pre-commit" -version = "2.21.0" +version = "3.3.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, - {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, + {file = "pre_commit-3.3.2-py2.py3-none-any.whl", hash = "sha256:8056bc52181efadf4aac792b1f4f255dfd2fb5a350ded7335d251a68561e8cb6"}, + {file = "pre_commit-3.3.2.tar.gz", hash = "sha256:66e37bec2d882de1f17f88075047ef8962581f83c234ac08da21a0c58953d1f0"}, ] [package.dependencies] @@ -2158,38 +2177,37 @@ virtualenv = ">=20.10.0" [[package]] name = "prefixed" -version = "0.6.0" +version = "0.7.0" description = "Prefixed alternative numeric library" category = "main" optional = false python-versions = "*" files = [ - {file = "prefixed-0.6.0-py2.py3-none-any.whl", hash = "sha256:5ab094773dc71df68cc78151c81510b9521dcc6b58a4acb78442b127d4e400fa"}, - {file = "prefixed-0.6.0.tar.gz", hash = "sha256:b39fbfac72618fa1eeb5b3fd9ed1341f10dd90df75499cb4c38a6c3ef47cdd94"}, + {file = "prefixed-0.7.0-py2.py3-none-any.whl", hash = "sha256:537b0e4ff4516c4578f277a41d7104f769d6935ae9cdb0f88fed82ec7b3c0ca5"}, + {file = "prefixed-0.7.0.tar.gz", hash = "sha256:0b54d15e602eb8af4ac31b1db21a37ea95ce5890e0741bb0dd9ded493cefbbe9"}, ] [[package]] name = "protobuf" -version = "4.21.12" +version = "4.23.2" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.21.12-cp310-abi3-win32.whl", hash = "sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1"}, - {file = "protobuf-4.21.12-cp310-abi3-win_amd64.whl", hash = "sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2"}, - {file = "protobuf-4.21.12-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791"}, - {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97"}, - {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7"}, - {file = "protobuf-4.21.12-cp37-cp37m-win32.whl", hash = "sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717"}, - {file = "protobuf-4.21.12-cp37-cp37m-win_amd64.whl", hash = "sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"}, - {file = "protobuf-4.21.12-cp38-cp38-win32.whl", hash = "sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec"}, - {file = "protobuf-4.21.12-cp38-cp38-win_amd64.whl", hash = "sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30"}, - {file = "protobuf-4.21.12-cp39-cp39-win32.whl", hash = "sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc"}, - {file = "protobuf-4.21.12-cp39-cp39-win_amd64.whl", hash = "sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b"}, - {file = "protobuf-4.21.12-py2.py3-none-any.whl", hash = "sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5"}, - {file = "protobuf-4.21.12-py3-none-any.whl", hash = "sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462"}, - {file = "protobuf-4.21.12.tar.gz", hash = "sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab"}, + {file = "protobuf-4.23.2-cp310-abi3-win32.whl", hash = "sha256:384dd44cb4c43f2ccddd3645389a23ae61aeb8cfa15ca3a0f60e7c3ea09b28b3"}, + {file = "protobuf-4.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:09310bce43353b46d73ba7e3bca78273b9bc50349509b9698e64d288c6372c2a"}, + {file = "protobuf-4.23.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b2cfab63a230b39ae603834718db74ac11e52bccaaf19bf20f5cce1a84cf76df"}, + {file = "protobuf-4.23.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:c52cfcbfba8eb791255edd675c1fe6056f723bf832fa67f0442218f8817c076e"}, + {file = "protobuf-4.23.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:86df87016d290143c7ce3be3ad52d055714ebaebb57cc659c387e76cfacd81aa"}, + {file = "protobuf-4.23.2-cp37-cp37m-win32.whl", hash = "sha256:281342ea5eb631c86697e1e048cb7e73b8a4e85f3299a128c116f05f5c668f8f"}, + {file = "protobuf-4.23.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ce744938406de1e64b91410f473736e815f28c3b71201302612a68bf01517fea"}, + {file = "protobuf-4.23.2-cp38-cp38-win32.whl", hash = "sha256:6c081863c379bb1741be8f8193e893511312b1d7329b4a75445d1ea9955be69e"}, + {file = "protobuf-4.23.2-cp38-cp38-win_amd64.whl", hash = "sha256:25e3370eda26469b58b602e29dff069cfaae8eaa0ef4550039cc5ef8dc004511"}, + {file = "protobuf-4.23.2-cp39-cp39-win32.whl", hash = "sha256:efabbbbac1ab519a514579ba9ec52f006c28ae19d97915951f69fa70da2c9e91"}, + {file = "protobuf-4.23.2-cp39-cp39-win_amd64.whl", hash = "sha256:54a533b971288af3b9926e53850c7eb186886c0c84e61daa8444385a4720297f"}, + {file = "protobuf-4.23.2-py3-none-any.whl", hash = "sha256:8da6070310d634c99c0db7df48f10da495cc283fd9e9234877f0cd182d43ab7f"}, + {file = "protobuf-4.23.2.tar.gz", hash = "sha256:20874e7ca4436f683b64ebdbee2129a5a2c301579a67d1a7dda2cdf62fb7f5f7"}, ] [[package]] @@ -2217,41 +2235,41 @@ files = [ [[package]] name = "pyasn1" -version = "0.4.8" -description = "ASN.1 types and codecs" +version = "0.5.0" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" category = "main" optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, + {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, + {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, ] [[package]] name = "pyasn1-modules" -version = "0.2.8" -description = "A collection of ASN.1-based protocols modules." +version = "0.3.0" +description = "A collection of ASN.1-based protocols modules" category = "main" optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, - {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, + {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, + {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.5.0" +pyasn1 = ">=0.4.6,<0.6.0" [[package]] name = "pyblish-base" -version = "1.8.8" +version = "1.8.11" description = "Plug-in driven automation framework for content" category = "main" optional = false python-versions = "*" files = [ - {file = "pyblish-base-1.8.8.tar.gz", hash = "sha256:85a2c034dbb86345bf95018f5b7b3c36c7dda29ea4d93c10d167f147b69a7b22"}, - {file = "pyblish_base-1.8.8-py2.py3-none-any.whl", hash = "sha256:67ea253a05d007ab4a175e44e778928ea7bdb0e9707573e1100417bbf0451a53"}, + {file = "pyblish-base-1.8.11.tar.gz", hash = "sha256:86dfeec0567430eb7eb25f89a18312054147a729ec66f6ac8c7e421fd15b66e1"}, + {file = "pyblish_base-1.8.11-py2.py3-none-any.whl", hash = "sha256:c321be7020c946fe9dfa11941241bd985a572c5009198b4f9810e5afad1f0b4b"}, ] [[package]] @@ -2310,14 +2328,14 @@ files = [ [[package]] name = "pygments" -version = "2.14.0" +version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, - {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, + {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, + {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, ] [package.extras] @@ -2325,18 +2343,18 @@ plugins = ["importlib-metadata"] [[package]] name = "pylint" -version = "2.15.10" +version = "2.17.4" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "pylint-2.15.10-py3-none-any.whl", hash = "sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e"}, - {file = "pylint-2.15.10.tar.gz", hash = "sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5"}, + {file = "pylint-2.17.4-py3-none-any.whl", hash = "sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c"}, + {file = "pylint-2.17.4.tar.gz", hash = "sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1"}, ] [package.dependencies] -astroid = ">=2.12.13,<=2.14.0-dev0" +astroid = ">=2.15.4,<=2.17.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = {version = ">=0.2", markers = "python_version < \"3.11\""} isort = ">=4.2.5,<6" @@ -2527,83 +2545,83 @@ six = "*" [[package]] name = "pyobjc-core" -version = "9.0.1" +version = "9.1.1" description = "Python<->ObjC Interoperability Module" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pyobjc-core-9.0.1.tar.gz", hash = "sha256:5ce1510bb0bdff527c597079a42b2e13a19b7592e76850be7960a2775b59c929"}, - {file = "pyobjc_core-9.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b614406d46175b1438a9596b664bf61952323116704d19bc1dea68052a0aad98"}, - {file = "pyobjc_core-9.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bd397e729f6271c694fb70df8f5d3d3c9b2f2b8ac02fbbdd1757ca96027b94bb"}, - {file = "pyobjc_core-9.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d919934eaa6d1cf1505ff447a5c2312be4c5651efcb694eb9f59e86f5bd25e6b"}, - {file = "pyobjc_core-9.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:67d67ca8b164f38ceacce28a18025845c3ec69613f3301935d4d2c4ceb22e3fd"}, - {file = "pyobjc_core-9.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:39d11d71f6161ac0bd93cffc8ea210bb0178b56d16a7408bf74283d6ecfa7430"}, - {file = "pyobjc_core-9.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25be1c4d530e473ed98b15063b8d6844f0733c98914de6f09fe1f7652b772bbc"}, + {file = "pyobjc-core-9.1.1.tar.gz", hash = "sha256:4b6cb9053b5fcd3c0e76b8c8105a8110786b20f3403c5643a688c5ec51c55c6b"}, + {file = "pyobjc_core-9.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4bd07049fd9fe5b40e4b7c468af9cf942508387faf383a5acb043d20627bad2c"}, + {file = "pyobjc_core-9.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a8307527621729ff2ab67860e7ed84f76ad0da881b248c2ef31e0da0088e4ba"}, + {file = "pyobjc_core-9.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:083004d28b92ccb483a41195c600728854843b0486566aba2d6e63eef51f80e6"}, + {file = "pyobjc_core-9.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d61e9517d451bc062a7fae8b3648f4deba4fa54a24926fa1cf581b90ef4ced5a"}, + {file = "pyobjc_core-9.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1626909916603a3b04c07c721cf1af0e0b892cec85bb3db98d05ba024f1786fc"}, + {file = "pyobjc_core-9.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2dde96462b52e952515d142e2afbb6913624a02c13582047e06211e6c3993728"}, ] [[package]] name = "pyobjc-framework-applicationservices" -version = "9.0.1" +version = "9.1.1" description = "Wrappers for the framework ApplicationServices on macOS" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pyobjc-framework-ApplicationServices-9.0.1.tar.gz", hash = "sha256:e3a350781fdcab6c1da4343dfc54ae3c0523e59e61147432f61dcfb365752fde"}, - {file = "pyobjc_framework_ApplicationServices-9.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4214febf3cc2e417ae15d45b6502e5c20f1097cd042b025760d019fe69b07b6"}, - {file = "pyobjc_framework_ApplicationServices-9.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c62693e01ba272fbadcd66677881311d2d63fda84b9662533fcc883c54be76d7"}, - {file = "pyobjc_framework_ApplicationServices-9.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6829df4dc4cf012bdc221d4e0296d6699b33ca89741569df153989a0c18aa40e"}, - {file = "pyobjc_framework_ApplicationServices-9.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5af5d12871499c429dd68c5ec4be56c631ec8439aa953c266eed9afdffb5ec2b"}, - {file = "pyobjc_framework_ApplicationServices-9.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:724da9dfae6ab0505b90340231a685720288caecfcca335b08903102e97a93dc"}, - {file = "pyobjc_framework_ApplicationServices-9.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8e1dbfc8f482c433ce642724d4bed0c527c7f2f2f8b9ba1ac3f778a68cf1538d"}, + {file = "pyobjc-framework-ApplicationServices-9.1.1.tar.gz", hash = "sha256:50c613bee364150bbd6cd992ca32b0848a780922cb57d112f6a4a56e29802e19"}, + {file = "pyobjc_framework_ApplicationServices-9.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9286c05d80a6aafc7388a4c2a35801db9ea6bab960acf2df079110debb659cb"}, + {file = "pyobjc_framework_ApplicationServices-9.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3db1c79d7420052320529432e8562cd339a7ef0841df83a85bbf3648abb55b6b"}, + {file = "pyobjc_framework_ApplicationServices-9.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:baf5a0d72c9e2d2a3b402823a2ea53eccdc27b8b9319d61cee7d753a30cb9411"}, + {file = "pyobjc_framework_ApplicationServices-9.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7cc5aad93bb6b178f838fe9b78cdcf1217c7baab157b1f3525e0acf696cc3490"}, + {file = "pyobjc_framework_ApplicationServices-9.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a03434605873b9f83255a0b16bbc539d06afd77f5969a3b11a1fc293dfd56680"}, + {file = "pyobjc_framework_ApplicationServices-9.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9f18e9b92674be0e503a2bd451a328693450e6f80ee510bbc375238b14117e24"}, ] [package.dependencies] -pyobjc-core = ">=9.0.1" -pyobjc-framework-Cocoa = ">=9.0.1" -pyobjc-framework-Quartz = ">=9.0.1" +pyobjc-core = ">=9.1.1" +pyobjc-framework-Cocoa = ">=9.1.1" +pyobjc-framework-Quartz = ">=9.1.1" [[package]] name = "pyobjc-framework-cocoa" -version = "9.0.1" +version = "9.1.1" description = "Wrappers for the Cocoa frameworks on macOS" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pyobjc-framework-Cocoa-9.0.1.tar.gz", hash = "sha256:a8b53b3426f94307a58e2f8214dc1094c19afa9dcb96f21be12f937d968b2df3"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f94b0f92a62b781e633e58f09bcaded63d612f9b1e15202f5f372ea59e4aebd"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f062c3bb5cc89902e6d164aa9a66ffc03638645dd5f0468b6f525ac997c86e51"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b374c0a9d32ba4fc5610ab2741cb05a005f1dfb82a47dbf2dbb2b3a34b73ce5"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8928080cebbce91ac139e460d3dfc94c7cb6935be032dcae9c0a51b247f9c2d9"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:9d2bd86a0a98d906f762f5dc59f2fc67cce32ae9633b02ff59ac8c8a33dd862d"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2a41053cbcee30e1e8914efa749c50b70bf782527d5938f2bc2a6393740969ce"}, + {file = "pyobjc-framework-Cocoa-9.1.1.tar.gz", hash = "sha256:345c32b6d1f3db45f635e400f2d0d6c0f0f7349d45ec823f76fc1df43d13caeb"}, + {file = "pyobjc_framework_Cocoa-9.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9176a4276f3b4b4758e9b9ca10698be5341ceffaeaa4fa055133417179e6bc37"}, + {file = "pyobjc_framework_Cocoa-9.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5e1e96fb3461f46ff951413515f2029e21be268b0e033db6abee7b64ec8e93d3"}, + {file = "pyobjc_framework_Cocoa-9.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:083b195c496d30c6b9dd86126a6093c4b95e0138e9b052b13e54103fcc0b4872"}, + {file = "pyobjc_framework_Cocoa-9.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a1b3333b1aa045608848bd68bbab4c31171f36aeeaa2fabeb4527c6f6f1e33cd"}, + {file = "pyobjc_framework_Cocoa-9.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:54c017354671f0d955432986c42218e452ca69906a101c8e7acde8510432303a"}, + {file = "pyobjc_framework_Cocoa-9.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:10c0075688ce95b92caf59e368585fffdcc98c919bc345067af070222f5d01d2"}, ] [package.dependencies] -pyobjc-core = ">=9.0.1" +pyobjc-core = ">=9.1.1" [[package]] name = "pyobjc-framework-quartz" -version = "9.0.1" +version = "9.1.1" description = "Wrappers for the Quartz frameworks on macOS" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pyobjc-framework-Quartz-9.0.1.tar.gz", hash = "sha256:7e2e37fc5c01bbdc37c1355d886e6184d1977043d5a05d1d956573fa8503dac3"}, - {file = "pyobjc_framework_Quartz-9.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:13a546a2af7c1c5c2bbf88cce6891896a449e92466415ad14d9a5ee93fba6ef3"}, - {file = "pyobjc_framework_Quartz-9.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:93ee6e339ab6928115a92188a0162ec80bf62cd0bd908d54695c1b9f9381ea45"}, - {file = "pyobjc_framework_Quartz-9.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:066ffbe26de1456f79a6d9467dabd6a3b9ef228318a0ba3f3fedbdbc0e2d3444"}, - {file = "pyobjc_framework_Quartz-9.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9b553be6ef672e0886b0d2c77d1841b1a942c7b1dc9a67f6e1376dc5493513"}, - {file = "pyobjc_framework_Quartz-9.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7b39f85d0b747b0a13a11d0d538001b757c82d05e656eab437167b5b118307df"}, - {file = "pyobjc_framework_Quartz-9.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0bedb6e1b7789d5b24fd5c790f0d53e4c62930313c97a891068bfa0e966ccc0b"}, + {file = "pyobjc-framework-Quartz-9.1.1.tar.gz", hash = "sha256:8d03bc52bd6d90f00f274fd709b82e53dc5dfca19f3fc744997634e03faaa159"}, + {file = "pyobjc_framework_Quartz-9.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32602f46353a5eadb0843a0940635c8ec103f47d5b1ce84284604e01c6393fa8"}, + {file = "pyobjc_framework_Quartz-9.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b3a56f52f9bb7fbd45c5a5f0de312ee9c104dfce6e1731015048d9e65a95e43"}, + {file = "pyobjc_framework_Quartz-9.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b3138773dfb269e6e3894e20dcfaf90102bad84ba44aa2bba8683b8426a69cdd"}, + {file = "pyobjc_framework_Quartz-9.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b583e6953e9c65525db908c33c1c97cead3ac8aa0cf2759fcc568666a1b7373"}, + {file = "pyobjc_framework_Quartz-9.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:c3efcbba62e9c5351c2a9469faabb7f400f214cd8cf98f57798d6b6c93c76efb"}, + {file = "pyobjc_framework_Quartz-9.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a82d43c6c5fe0f5d350cfc97212bef7c572e345aa9c6e23909d23dace6448c99"}, ] [package.dependencies] -pyobjc-core = ">=9.0.1" -pyobjc-framework-Cocoa = ">=9.0.1" +pyobjc-core = ">=9.1.1" +pyobjc-framework-Cocoa = ">=9.1.1" [[package]] name = "pyparsing" @@ -2658,14 +2676,14 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-cov" -version = "4.0.0" +version = "4.1.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, - {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] @@ -2710,14 +2728,14 @@ six = ">=1.5" [[package]] name = "python-engineio" -version = "4.4.0" +version = "4.4.1" description = "Engine.IO server and client for Python" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "python-engineio-4.4.0.tar.gz", hash = "sha256:bcc035c70ecc30acc3cfd49ef19aca6c51fa6caaadd0fa58c2d7480f50d04cf2"}, - {file = "python_engineio-4.4.0-py3-none-any.whl", hash = "sha256:11f9c35b775fe70e0a25f67b16d5b69fbfafc368cdd87eeb6f4135a475c88e50"}, + {file = "python-engineio-4.4.1.tar.gz", hash = "sha256:eb3663ecb300195926b526386f712dff84cd092c818fb7b62eeeda9160120c29"}, + {file = "python_engineio-4.4.1-py3-none-any.whl", hash = "sha256:28ab67f94cba2e5f598cbb04428138fd6bb8b06d3478c939412da445f24f0773"}, ] [package.extras] @@ -2772,18 +2790,6 @@ files = [ {file = "python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8"}, ] -[[package]] -name = "pytz" -version = "2022.7.1" -description = "World timezone definitions, modern and historical" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, - {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, -] - [[package]] name = "pywin32" version = "301" @@ -2868,16 +2874,19 @@ files = [ [[package]] name = "qt-py" -version = "1.3.7" +version = "1.3.8" description = "Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5." category = "main" optional = false python-versions = "*" files = [ - {file = "Qt.py-1.3.7-py2.py3-none-any.whl", hash = "sha256:150099d1c6f64c9621a2c9d79d45102ec781c30ee30ee69fc082c6e9be7324fe"}, - {file = "Qt.py-1.3.7.tar.gz", hash = "sha256:803c7bdf4d6230f9a466be19d55934a173eabb61406d21cb91e80c2a3f773b1f"}, + {file = "Qt.py-1.3.8-py2.py3-none-any.whl", hash = "sha256:665b9d4cfefaff2d697876d5027e145a0e0b1ba62dda9652ea114db134bc9911"}, + {file = "Qt.py-1.3.8.tar.gz", hash = "sha256:6d330928f7ec8db8e329b19116c52482b6abfaccfa5edef0248e57d012300895"}, ] +[package.dependencies] +types-PySide2 = "*" + [[package]] name = "qtawesome" version = "0.7.3" @@ -2896,14 +2905,14 @@ six = "*" [[package]] name = "qtpy" -version = "2.3.0" +version = "2.3.1" description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "QtPy-2.3.0-py3-none-any.whl", hash = "sha256:8d6d544fc20facd27360ea189592e6135c614785f0dec0b4f083289de6beb408"}, - {file = "QtPy-2.3.0.tar.gz", hash = "sha256:0603c9c83ccc035a4717a12908bf6bc6cb22509827ea2ec0e94c2da7c9ed57c5"}, + {file = "QtPy-2.3.1-py3-none-any.whl", hash = "sha256:5193d20e0b16e4d9d3bc2c642d04d9f4e2c892590bd1b9c92bfe38a95d5a2e12"}, + {file = "QtPy-2.3.1.tar.gz", hash = "sha256:a8c74982d6d172ce124d80cafd39653df78989683f760f2281ba91a6e7b9de8b"}, ] [package.dependencies] @@ -2931,30 +2940,30 @@ sphinx = ">=1.3.1" [[package]] name = "requests" -version = "2.28.1" +version = "2.31.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=3.7, <4" +python-versions = ">=3.7" files = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" +urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] -name = "revitron_sphinx_theme" +name = "revitron-sphinx-theme" version = "0.7.2" -description = "" +description = "Revitron theme for Sphinx" category = "dev" optional = false python-versions = "*" @@ -3063,19 +3072,19 @@ files = [ [[package]] name = "slack-sdk" -version = "3.19.5" +version = "3.21.3" description = "The Slack API Platform SDK for Python" category = "main" optional = false python-versions = ">=3.6.0" files = [ - {file = "slack_sdk-3.19.5-py2.py3-none-any.whl", hash = "sha256:0b52bb32a87c71f638b9eb47e228dffeebf89de5e762684ef848276f9f186c84"}, - {file = "slack_sdk-3.19.5.tar.gz", hash = "sha256:47fb4af596243fe6585a92f3034de21eb2104a55cc9fd59a92ef3af17cf9ddd8"}, + {file = "slack_sdk-3.21.3-py2.py3-none-any.whl", hash = "sha256:de3c07b92479940b61cd68c566f49fbc9974c8f38f661d26244078f3903bb9cc"}, + {file = "slack_sdk-3.21.3.tar.gz", hash = "sha256:20829bdc1a423ec93dac903470975ebf3bc76fd3fd91a4dadc0eeffc940ecb0c"}, ] [package.extras] -optional = ["SQLAlchemy (>=1,<2)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"] -testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "codecov (>=2,<3)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"] +optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"] +testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"] [[package]] name = "smmap" @@ -3151,21 +3160,21 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-autoapi" -version = "2.0.1" +version = "2.1.0" description = "Sphinx API documentation generator" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "sphinx-autoapi-2.0.1.tar.gz", hash = "sha256:cdf47968c20852f4feb0ccefd09e414bb820af8af8f82fab15a24b09a3d1baba"}, - {file = "sphinx_autoapi-2.0.1-py2.py3-none-any.whl", hash = "sha256:8ed197a0c9108770aa442a5445744c1405b356ea64df848e8553411b9b9e129b"}, + {file = "sphinx-autoapi-2.1.0.tar.gz", hash = "sha256:5b5c58064214d5a846c9c81d23f00990a64654b9bca10213231db54a241bc50f"}, + {file = "sphinx_autoapi-2.1.0-py2.py3-none-any.whl", hash = "sha256:b25c7b2cda379447b8c36b6a0e3bdf76e02fd64f7ca99d41c6cbdf130a01768f"}, ] [package.dependencies] astroid = ">=2.7" Jinja2 = "*" PyYAML = "*" -sphinx = ">=4.0" +sphinx = ">=5.2.0" unidecode = "*" [package.extras] @@ -3175,14 +3184,14 @@ go = ["sphinxcontrib-golangdomain"] [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.3" +version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "sphinxcontrib.applehelp-1.0.3-py3-none-any.whl", hash = "sha256:ba0f2a22e6eeada8da6428d0d520215ee8864253f32facf958cca81e426f661d"}, - {file = "sphinxcontrib.applehelp-1.0.3.tar.gz", hash = "sha256:83749f09f6ac843b8cb685277dbc818a8bf2d76cc19602699094fe9a74db529e"}, + {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, + {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, ] [package.extras] @@ -3207,14 +3216,14 @@ test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.0" +version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, + {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, + {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, ] [package.extras] @@ -3338,38 +3347,49 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.6" +version = "0.11.8" description = "Style preserving TOML library" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, - {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, + {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, + {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, +] + +[[package]] +name = "types-pyside2" +version = "5.15.2.1.5" +description = "The most accurate stubs for PySide2" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "types_PySide2-5.15.2.1.5-py2.py3-none-any.whl", hash = "sha256:4bbee2c8f09961101013d05bb5c506b7351b3020494fc8b5c3b73c95014fa1b0"}, ] [[package]] name = "typing-extensions" -version = "4.4.0" +version = "4.6.2" description = "Backported and Experimental Type Hints for Python 3.7+" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, + {file = "typing_extensions-4.6.2-py3-none-any.whl", hash = "sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"}, + {file = "typing_extensions-4.6.2.tar.gz", hash = "sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c"}, ] [[package]] name = "uc-micro-py" -version = "1.0.1" +version = "1.0.2" description = "Micro subset of unicode data files for linkify-it-py projects." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "uc-micro-py-1.0.1.tar.gz", hash = "sha256:b7cdf4ea79433043ddfe2c82210208f26f7962c0cfbe3bacb05ee879a7fdb596"}, - {file = "uc_micro_py-1.0.1-py3-none-any.whl", hash = "sha256:316cfb8b6862a0f1d03540f0ae6e7b033ff1fa0ddbe60c12cbe0d4cec846a69f"}, + {file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"}, + {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"}, ] [package.extras] @@ -3377,14 +3397,14 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "unidecode" -version = "1.2.0" +version = "1.3.6" description = "ASCII transliterations of Unicode text" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" files = [ - {file = "Unidecode-1.2.0-py2.py3-none-any.whl", hash = "sha256:12435ef2fc4cdfd9cf1035a1db7e98b6b047fe591892e81f34e94959591fad00"}, - {file = "Unidecode-1.2.0.tar.gz", hash = "sha256:8d73a97d387a956922344f6b74243c2c6771594659778744b2dbdaad8f6b727d"}, + {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, + {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, ] [[package]] @@ -3401,41 +3421,42 @@ files = [ [[package]] name = "urllib3" -version = "1.26.14" +version = "2.0.2" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7" files = [ - {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, - {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, + {file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"}, + {file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.17.1" +version = "20.23.0" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, - {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, + {file = "virtualenv-20.23.0-py3-none-any.whl", hash = "sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e"}, + {file = "virtualenv-20.23.0.tar.gz", hash = "sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924"}, ] [package.dependencies] distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<3" +filelock = ">=3.11,<4" +platformdirs = ">=3.2,<4" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] -testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.7.1)", "time-machine (>=2.9)"] [[package]] name = "wcwidth" @@ -3466,91 +3487,102 @@ six = "*" [[package]] name = "wheel" -version = "0.38.4" +version = "0.40.0" description = "A built-package format for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"}, - {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"}, + {file = "wheel-0.40.0-py3-none-any.whl", hash = "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"}, + {file = "wheel-0.40.0.tar.gz", hash = "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873"}, ] [package.extras] -test = ["pytest (>=3.0.0)"] +test = ["pytest (>=6.0.0)"] [[package]] name = "wrapt" -version = "1.14.1" +version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, ] [[package]] @@ -3576,86 +3608,86 @@ ujson = ["ujson"] [[package]] name = "yarl" -version = "1.8.2" +version = "1.9.2" description = "Yet another URL library" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"}, - {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"}, - {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"}, - {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"}, - {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"}, - {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"}, - {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"}, - {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"}, - {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"}, - {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"}, - {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"}, - {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"}, - {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"}, - {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"}, - {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"}, - {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"}, - {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"}, - {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"}, - {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"}, - {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"}, - {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"}, - {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"}, - {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"}, - {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, + {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, + {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, + {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, + {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, + {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, + {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, + {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, + {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, + {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, + {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, + {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, + {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, ] [package.dependencies] @@ -3664,19 +3696,19 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.11.0" +version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] docs = [] @@ -3684,4 +3716,4 @@ docs = [] [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "03c6c9ee661cc1de568875033d161478b490203f567d13675e0935dba146ecc8" +content-hash = "cdf2ba74b9838635baddfd5d79ea94e10243db328fe6dc426455475b8a047671" diff --git a/pyproject.toml b/pyproject.toml index 5dd67c0aae..63e9f4cd13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ appdirs = { git = "https://github.com/ActiveState/appdirs.git", branch = "master blessed = "^1.17" # openpype terminal formatting coolname = "*" clique = "1.6.*" -Click = "^7" +Click = "^8" dnspython = "^2.1.0" ftrack-python-api = "^2.3.3" arrow = "^0.17" From b48e7ebfe4bec022afb56b32e56e24ca66618333 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Jun 2023 10:10:24 +0200 Subject: [PATCH 235/446] AYON: Preparation for products (#5038) * removed settings of IntegrateAssetNew * added 'active' to 'ValidateEditorialAssetName' plugin settings * removed unused 'families_to_review' setting from tvpain * implemented product name -> subset and product type -> family conversion * fixed most of conversion utils related to subsets * removed unused constants * anatomy templates are handling folder and product in templates * handle all possible template changes of asset, subest and family in settings * updated ayon api * updated fixed ayon api * added conversion functions for representations * fixed 'get_thumbnail' compatibility * use del to handle ayon mode in intput links * changed labels in UI based on AYON mode * updated ayon_api with 0.2.0 release code --- openpype/client/mongo/entities.py | 4 +- openpype/client/server/constants.py | 65 -- openpype/client/server/conversion_utils.py | 138 ++- openpype/client/server/entities.py | 28 +- openpype/client/server/openpype_comp.py | 4 +- openpype/client/server/operations.py | 20 +- .../plugins/publish/integrate_inputlinks.py | 8 +- .../publish/integrate_inputlinks_ayon.py | 8 +- openpype/settings/ayon_settings.py | 241 ++++- .../defaults/project_settings/global.json | 68 +- .../defaults/project_settings/tvpaint.json | 5 - .../schema_project_tvpaint.json | 12 - .../schemas/schema_global_publish.json | 110 +- .../schemas/schema_global_tools.json | 4 - openpype/tools/creator/widgets.py | 28 +- openpype/tools/loader/app.py | 2 +- openpype/tools/loader/model.py | 7 +- .../tools/publisher/widgets/assets_widget.py | 4 +- .../tools/publisher/widgets/create_widget.py | 5 +- openpype/tools/publisher/widgets/widgets.py | 13 +- .../standalonepublish/widgets/widget_asset.py | 4 +- openpype/tools/utils/assets_widget.py | 4 +- .../vendor/python/common/ayon_api/__init__.py | 52 +- .../vendor/python/common/ayon_api/_api.py | 73 +- .../python/common/ayon_api/constants.py | 15 +- .../python/common/ayon_api/entity_hub.py | 6 +- .../python/common/ayon_api/graphql_queries.py | 99 +- .../python/common/ayon_api/operations.py | 105 +- .../python/common/ayon_api/server_api.py | 987 ++++++++++-------- .../vendor/python/common/ayon_api/utils.py | 2 +- .../vendor/python/common/ayon_api/version.py | 2 +- 31 files changed, 1194 insertions(+), 929 deletions(-) diff --git a/openpype/client/mongo/entities.py b/openpype/client/mongo/entities.py index adbdd7a47c..260fde4594 100644 --- a/openpype/client/mongo/entities.py +++ b/openpype/client/mongo/entities.py @@ -1468,7 +1468,9 @@ def get_thumbnails(project_name, thumbnail_ids, fields=None): return conn.find(query_filter, _prepare_fields(fields)) -def get_thumbnail(project_name, thumbnail_id, fields=None): +def get_thumbnail( + project_name, thumbnail_id, entity_type, entity_id, fields=None +): """Receive thumbnail entity data. Args: diff --git a/openpype/client/server/constants.py b/openpype/client/server/constants.py index 7ff990dc60..1d3f94c702 100644 --- a/openpype/client/server/constants.py +++ b/openpype/client/server/constants.py @@ -1,13 +1,3 @@ -# --- Project --- -DEFAULT_PROJECT_FIELDS = { - "active", - "name", - "code", - "config", - "data", - "createdAt", -} - # --- Folders --- DEFAULT_FOLDER_FIELDS = { "id", @@ -19,47 +9,6 @@ DEFAULT_FOLDER_FIELDS = { "thumbnailId" } -# --- Tasks --- -DEFAULT_TASK_FIELDS = { - "id", - "name", - "taskType", - "assignees", -} - -# --- Subsets --- -DEFAULT_SUBSET_FIELDS = { - "id", - "name", - "active", - "family", - "folderId", -} - -# --- Versions --- -DEFAULT_VERSION_FIELDS = { - "id", - "name", - "version", - "active", - "subsetId", - "taskId", - "author", - "thumbnailId", - "createdAt", - "updatedAt", -} - -# --- Representations --- -DEFAULT_REPRESENTATION_FIELDS = { - "id", - "name", - "context", - "createdAt", - "active", - "versionId", -} - REPRESENTATION_FILES_FIELDS = { "files.name", "files.hash", @@ -67,17 +16,3 @@ REPRESENTATION_FILES_FIELDS = { "files.path", "files.size", } - -DEFAULT_WORKFILE_INFO_FIELDS = { - "active", - "createdAt", - "createdBy", - "id", - "name", - "path", - "projectName", - "taskId", - "thumbnailId", - "updatedAt", - "updatedBy", -} diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index 54b89275fe..b5bd755470 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -56,7 +56,7 @@ SUBSET_FIELDS_MAPPING_V3_V4 = { VERSION_FIELDS_MAPPING_V3_V4 = { "_id": {"id"}, "name": {"version"}, - "parent": {"subsetId"} + "parent": {"productId"} } # --- Representation entity --- @@ -130,10 +130,21 @@ def _get_default_template_name(templates): return default_template +def _template_replacements_to_v3(template): + return ( + template + .replace("{folder[name]}", "{asset}") + .replace("{product[name]}", "{subset}") + .replace("{product[type]}", "{family}") + ) + + def _convert_template_item(template): - template["folder"] = template.pop("directory") + folder = _template_replacements_to_v3(template.pop("directory")) + template["folder"] = folder + template["file"] = _template_replacements_to_v3(template["file"]) template["path"] = "/".join( - (template["folder"], template["file"]) + (folder, template["file"]) ) @@ -384,7 +395,7 @@ def subset_fields_v3_to_v4(fields, con): if not fields: return None - subset_attributes = con.get_attributes_for_type("subset") + product_attributes = con.get_attributes_for_type("product") output = set() for field in fields: @@ -395,11 +406,11 @@ def subset_fields_v3_to_v4(fields, con): output |= SUBSET_FIELDS_MAPPING_V3_V4[field] elif field == "data": - output.add("family") + output.add("productType") output.add("active") output |= { "attrib.{}".format(attr) - for attr in subset_attributes + for attr in product_attributes } elif field.startswith("data"): @@ -407,9 +418,9 @@ def subset_fields_v3_to_v4(fields, con): field_parts.pop(0) data_key = ".".join(field_parts) if data_key in ("family", "families"): - output.add("family") + output.add("productType") - elif data_key in subset_attributes: + elif data_key in product_attributes: output.add("attrib.{}".format(data_key)) else: @@ -443,9 +454,11 @@ def convert_v4_subset_to_v3(subset): if "attrib" in subset: attrib = subset["attrib"] + if "productGroup" in attrib: + attrib["subsetGroup"] = attrib.pop("productGroup") output_data.update(attrib) - family = subset.get("family") + family = subset.get("productType") if family: output_data["family"] = family output_data["families"] = [family] @@ -537,8 +550,8 @@ def convert_v4_version_to_v3(version): "type": "hero_version", "schema": CURRENT_HERO_VERSION_SCHEMA, } - if "subsetId" in version: - output["parent"] = version["subsetId"] + if "productId" in version: + output["parent"] = version["productId"] if "data" in version: output["data"] = version["data"] @@ -550,8 +563,8 @@ def convert_v4_version_to_v3(version): "name": version_num, "schema": CURRENT_VERSION_SCHEMA } - if "subsetId" in version: - output["parent"] = version["subsetId"] + if "productId" in version: + output["parent"] = version["productId"] output_data = version.get("data") or {} if "attrib" in version: @@ -647,6 +660,16 @@ def convert_v4_representation_to_v3(representation): context = representation["context"] if isinstance(context, six.string_types): context = json.loads(context) + + if "folder" in context: + _c_folder = context.pop("folder") + context["asset"] = _c_folder["name"] + + if "product" in context: + _c_product = context.pop("product") + context["family"] = _c_product["type"] + context["subset"] = _c_product["name"] + output["context"] = context if "files" in representation: @@ -686,6 +709,14 @@ def convert_v4_representation_to_v3(representation): if key in representation: output_data[data_key] = representation[key] + if "template" in output_data: + output_data["template"] = ( + output_data["template"] + .replace("{folder[name]}", "{asset}") + .replace("{product[name]}", "{subset}") + .replace("{product[type]}", "{family}") + ) + output["data"] = output_data return output @@ -714,7 +745,7 @@ def workfile_info_fields_v3_to_v4(fields): def convert_v4_workfile_info_to_v3(workfile_info, task): output = { - "type": "representation", + "type": "workfile", "schema": CURRENT_WORKFILE_INFO_SCHEMA, } if "id" in workfile_info: @@ -790,44 +821,46 @@ def convert_create_task_to_v4(task, project, con): def convert_create_subset_to_v4(subset, con): - subset_attributes = con.get_attributes_for_type("subset") + product_attributes = con.get_attributes_for_type("product") subset_data = subset["data"] - family = subset_data.get("family") - if not family: - family = subset_data["families"][0] + product_type = subset_data.get("family") + if not product_type: + product_type = subset_data["families"][0] - converted_subset = { + converted_product = { "name": subset["name"], - "family": family, + "productType": product_type, "folderId": subset["parent"], } entity_id = subset.get("_id") if entity_id: - converted_subset["id"] = entity_id + converted_product["id"] = entity_id attribs = {} data = {} + if "subsetGroup" in subset_data: + subset_data["productGroup"] = subset_data.pop("subsetGroup") for key, value in subset_data.items(): - if key not in subset_attributes: + if key not in product_attributes: data[key] = value elif value is not None: attribs[key] = value if attribs: - converted_subset["attrib"] = attribs + converted_product["attrib"] = attribs if data: - converted_subset["data"] = data + converted_product["data"] = data - return converted_subset + return converted_product def convert_create_version_to_v4(version, con): version_attributes = con.get_attributes_for_type("version") converted_version = { "version": version["name"], - "subsetId": version["parent"], + "productId": version["parent"], } entity_id = version.get("_id") if entity_id: @@ -870,7 +903,7 @@ def convert_create_hero_version_to_v4(hero_version, project_name, con): version_attributes = con.get_attributes_for_type("version") converted_version = { "version": hero_version["version"], - "subsetId": hero_version["parent"], + "productId": hero_version["parent"], } entity_id = hero_version.get("_id") if entity_id: @@ -923,12 +956,28 @@ def convert_create_representation_to_v4(representation, con): new_files.append(new_file_item) converted_representation["files"] = new_files + + context = representation["context"] + context["folder"] = { + "name": context.pop("asset", None) + } + context["product"] = { + "type": context.pop("family", None), + "name": context.pop("subset", None), + } + attribs = {} data = { - "context": representation["context"], + "context": context, } representation_data = representation["data"] + representation_data["template"] = ( + representation_data["template"] + .replace("{asset}", "{folder[name]}") + .replace("{subset}", "{product[name]}") + .replace("{family}", "{product[type]}") + ) for key, value in representation_data.items(): if key not in representation_attributes: @@ -1073,7 +1122,7 @@ def convert_update_folder_to_v4(project_name, asset_id, update_data, con): def convert_update_subset_to_v4(project_name, subset_id, update_data, con): new_update_data = {} - subset_attributes = con.get_attributes_for_type("subset") + product_attributes = con.get_attributes_for_type("product") full_update_data = _from_flat_dict(update_data) data = full_update_data.get("data") new_data = {} @@ -1081,15 +1130,17 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con): if data: if "family" in data: family = data.pop("family") - new_update_data["family"] = family + new_update_data["productType"] = family if "families" in data: families = data.pop("families") - if "family" not in new_update_data: - new_update_data["family"] = families[0] + if "productType" not in new_update_data: + new_update_data["productType"] = families[0] + if "subsetGroup" in data: + data["productGroup"] = data.pop("subsetGroup") for key, value in data.items(): - if key in subset_attributes: + if key in product_attributes: if value is REMOVED_VALUE: value = None attribs[key] = value @@ -1159,7 +1210,7 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con): new_update_data["active"] = False if "parent" in update_data: - new_update_data["subsetId"] = update_data["parent"] + new_update_data["productId"] = update_data["parent"] flat_data = _to_flat_dict(new_update_data) if new_data: @@ -1209,6 +1260,14 @@ def convert_update_representation_to_v4( else: new_data[key] = value + if "template" in attribs: + attribs["template"] = ( + attribs["template"] + .replace("{asset}", "{folder[name]}") + .replace("{family}", "{product[type]}") + .replace("{subset}", "{product[name]}") + ) + if "name" in update_data: new_update_data["name"] = update_data["name"] @@ -1223,7 +1282,16 @@ def convert_update_representation_to_v4( new_update_data["versionId"] = update_data["parent"] if "context" in update_data: - new_data["context"] = update_data["context"] + context = update_data["context"] + if "asset" in context: + context["folder"] = {"name": context.pop("asset")} + + if "family" in context or "subset" in context: + context["product"] = { + "name": context.pop("subset"), + "type": context.pop("family"), + } + new_data["context"] = context if "files" in update_data: new_files = update_data["files"] diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 2a2662b327..fee503297b 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -79,7 +79,7 @@ def _get_subsets( if archived: active = False - for subset in con.get_subsets( + for subset in con.get_products( project_name, subset_ids, subset_names, @@ -105,11 +105,11 @@ def _get_versions( fields = version_fields_v3_to_v4(fields, con) - # Make sure 'subsetId' and 'version' are available when hero versions + # Make sure 'productId' and 'version' are available when hero versions # are queried if fields and hero: fields = set(fields) - fields |= {"subsetId", "version"} + fields |= {"productId", "version"} queried_versions = con.get_versions( project_name, @@ -135,22 +135,22 @@ def _get_versions( versions_nums = set() for hero_version in hero_versions: versions_nums.add(abs(hero_version["version"])) - subset_ids.add(hero_version["subsetId"]) + subset_ids.add(hero_version["productId"]) hero_eq_versions = con.get_versions( project_name, - subset_ids=subset_ids, + product_ids=subset_ids, versions=versions_nums, hero=False, - fields=["id", "version", "subsetId"] + fields=["id", "version", "productId"] ) hero_eq_by_subset_id = collections.defaultdict(list) for version in hero_eq_versions: - hero_eq_by_subset_id[version["subsetId"]].append(version) + hero_eq_by_subset_id[version["productId"]].append(version) for hero_version in hero_versions: abs_version = abs(hero_version["version"]) - subset_id = hero_version["subsetId"] + subset_id = hero_version["productId"] version_id = None for version in hero_eq_by_subset_id.get(subset_id, []): if version["version"] == abs_version: @@ -235,7 +235,7 @@ def get_archived_assets( def get_asset_ids_with_subsets(project_name, asset_ids=None): con = get_server_api_connection() - return con.get_asset_ids_with_subsets(project_name, asset_ids) + return con.get_folder_ids_with_products(project_name, asset_ids) def get_subset_by_id(project_name, subset_id, fields=None): @@ -281,7 +281,7 @@ def get_subsets( def get_subset_families(project_name, subset_ids=None): con = get_server_api_connection() - return con.get_subset_families(project_name, subset_ids) + return con.get_product_type_names(project_name, subset_ids) def get_version_by_id(project_name, version_id, fields=None): @@ -618,6 +618,14 @@ def get_thumbnail( def get_thumbnails(project_name, thumbnail_contexts, fields=None): + """Get thumbnail entities. + + Warning: + This function is not OpenPype compatible. There is none usage of this + function in codebase so there is nothing to convert. The previous + implementation cannot be AYON compatible without entity types. + """ + thumbnail_items = set() for thumbnail_context in thumbnail_contexts: thumbnail_id, entity_type, entity_id = thumbnail_context diff --git a/openpype/client/server/openpype_comp.py b/openpype/client/server/openpype_comp.py index df3ffcc0d3..a123fe3167 100644 --- a/openpype/client/server/openpype_comp.py +++ b/openpype/client/server/openpype_comp.py @@ -11,7 +11,7 @@ def folders_tasks_graphql_query(fields): parent_folder_ids_var = query.add_variable("parentFolderIds", "[String!]") folder_paths_var = query.add_variable("folderPaths", "[String!]") folder_names_var = query.add_variable("folderNames", "[String!]") - has_subsets_var = query.add_variable("folderHasSubsets", "Boolean!") + has_products_var = query.add_variable("folderHasProducts", "Boolean!") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) @@ -21,7 +21,7 @@ def folders_tasks_graphql_query(fields): folders_field.set_filter("parentIds", parent_folder_ids_var) folders_field.set_filter("names", folder_names_var) folders_field.set_filter("paths", folder_paths_var) - folders_field.set_filter("hasSubsets", has_subsets_var) + folders_field.set_filter("hasProducts", has_products_var) fields = set(fields) fields.discard("tasks") diff --git a/openpype/client/server/operations.py b/openpype/client/server/operations.py index e5254ee7b7..eeb55784e1 100644 --- a/openpype/client/server/operations.py +++ b/openpype/client/server/operations.py @@ -375,7 +375,18 @@ def prepare_representation_update_data(old_doc, new_doc, replace=True): Dict[str, Any]: Changes between old and new document. """ - return _prepare_update_data(old_doc, new_doc, replace) + changes = _prepare_update_data(old_doc, new_doc, replace) + context = changes.get("data", {}).get("context") + # Make sure that both 'family' and 'subset' are in changes if + # one of them changed (they'll both become 'product'). + if ( + context + and ("family" in context or "subset" in context) + ): + context["family"] = new_doc["data"]["context"]["family"] + context["subset"] = new_doc["data"]["context"]["subset"] + + return changes def prepare_workfile_info_update_data(old_doc, new_doc, replace=True): @@ -445,6 +456,7 @@ class ServerCreateOperation(CreateOperation): elif entity_type == "subset": new_data = convert_create_subset_to_v4(data, self.con) + entity_type = "product" elif entity_type == "version": new_data = convert_create_version_to_v4(data, self.con) @@ -551,6 +563,7 @@ class ServerUpdateOperation(UpdateOperation): new_update_data = convert_update_subset_to_v4( project_name, entity_id, update_data, self.con ) + entity_type = "product" elif entity_type == "version": new_update_data = convert_update_version_to_v4( @@ -636,9 +649,12 @@ class ServerDeleteOperation(DeleteOperation): if entity_type == "asset": entity_type == "folder" - if entity_type == "hero_version": + elif entity_type == "hero_version": entity_type = "version" + elif entity_type == "subset": + entity_type = "product" + super(ServerDeleteOperation, self).__init__( project_name, entity_type, entity_id ) diff --git a/openpype/plugins/publish/integrate_inputlinks.py b/openpype/plugins/publish/integrate_inputlinks.py index c639bbf994..3baa462a81 100644 --- a/openpype/plugins/publish/integrate_inputlinks.py +++ b/openpype/plugins/publish/integrate_inputlinks.py @@ -36,10 +36,6 @@ class IntegrateInputLinks(pyblish.api.ContextPlugin): """ - if AYON_SERVER_ENABLED: - self.log.info("Skipping, in AYON mode") - return - workfile = None publishing = [] @@ -139,3 +135,7 @@ class IntegrateInputLinks(pyblish.api.ContextPlugin): {"_id": version_doc["_id"]}, {"$set": {"data.inputLinks": input_links}} ) + + +if AYON_SERVER_ENABLED: + del IntegrateInputLinks diff --git a/openpype/plugins/publish/integrate_inputlinks_ayon.py b/openpype/plugins/publish/integrate_inputlinks_ayon.py index 8ab5c923c4..180524cd08 100644 --- a/openpype/plugins/publish/integrate_inputlinks_ayon.py +++ b/openpype/plugins/publish/integrate_inputlinks_ayon.py @@ -32,10 +32,6 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin): specific publish plugin. """ - if not AYON_SERVER_ENABLED: - self.log.info("Skipping, not in AYON mode") - return - workfile_instance, other_instances = self.split_instances(context) # Variable where links are stored in submethods @@ -158,3 +154,7 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin): output_id, "version" ) + + +if not AYON_SERVER_ENABLED: + del IntegrateInputLinksAYON diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index ae5f5ead4d..46ad77c7ca 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -351,18 +351,25 @@ def _convert_flame_project_settings(ayon_settings, output): ayon_flame = ayon_settings["flame"] ayon_publish_flame = ayon_flame["publish"] + # Plugin 'ExtractSubsetResources' renamed to 'ExtractProductResources' + if "ExtractSubsetResources" in ayon_publish_flame: + ayon_product_resources = ayon_publish_flame["ExtractSubsetResources"] + else: + ayon_product_resources = ( + ayon_publish_flame.pop("ExtractProductResources")) + ayon_publish_flame["ExtractSubsetResources"] = ayon_product_resources + # 'ExtractSubsetResources' changed model of 'export_presets_mapping' # - some keys were moved under 'other_parameters' - ayon_subset_resources = ayon_publish_flame["ExtractSubsetResources"] new_subset_resources = {} - for item in ayon_subset_resources.pop("export_presets_mapping"): + for item in ayon_product_resources.pop("export_presets_mapping"): name = item.pop("name") if "other_parameters" in item: other_parameters = item.pop("other_parameters") item.update(other_parameters) new_subset_resources[name] = item - ayon_subset_resources["export_presets_mapping"] = new_subset_resources + ayon_product_resources["export_presets_mapping"] = new_subset_resources # 'imageio' changed model # - missing subkey 'project' which is in root of 'imageio' model @@ -375,6 +382,21 @@ def _convert_flame_project_settings(ayon_settings, output): "profilesMapping": profile_mapping } + ayon_load_flame = ayon_flame["load"] + for plugin_name in ("LoadClip", "LoadClipBatch"): + plugin_settings = ayon_load_flame[plugin_name] + plugin_settings["families"] = plugin_settings.pop("product_types") + plugin_settings["clip_name_template"] = ( + plugin_settings["clip_name_template"] + .replace("{folder[name]}", "{asset}") + .replace("{product[name]}", "{subset}") + ) + plugin_settings["layer_rename_template"] = ( + plugin_settings["layer_rename_template"] + .replace("{folder[name]}", "{asset}") + .replace("{product[name]}", "{subset}") + ) + output["flame"] = ayon_flame @@ -409,6 +431,15 @@ def _convert_fusion_project_settings(ayon_settings, output): _convert_host_imageio(ayon_imageio_fusion) + ayon_create_saver = ayon_fusion["create"]["CreateSaver"] + ayon_create_saver["temp_rendering_path_template"] = ( + ayon_create_saver["temp_rendering_path_template"] + .replace("{product[name]}", "{subset}") + .replace("{product[type]}", "{family}") + .replace("{folder[name]}", "{asset}") + .replace("{task[name]}", "{task}") + ) + output["fusion"] = ayon_fusion @@ -477,6 +508,11 @@ def _convert_maya_project_settings(ayon_settings, output): SUFFIX_NAMING_TABLE ) + validate_frame_range = ayon_publish["ValidateFrameRange"] + if "exclude_product_types" in validate_frame_range: + validate_frame_range["exclude_families"] = ( + validate_frame_range.pop("exclude_product_types")) + # Extract playblast capture settings validate_rendern_settings = ayon_publish["ValidateRenderSettings"] for key in ( @@ -537,12 +573,31 @@ def _convert_maya_project_settings(ayon_settings, output): for item in renderer_settings["additional_options"] ] + # Workfile build + ayon_workfile_build = ayon_maya["workfile_build"] + for item in ayon_workfile_build["profiles"]: + for key in ("current_context", "linked_assets"): + for subitem in item[key]: + if "families" in subitem: + break + subitem["families"] = subitem.pop("product_types") + subitem["subset_name_filters"] = subitem.pop( + "product_name_filters") + _convert_host_imageio(ayon_maya) - load_colors = ayon_maya["load"]["colors"] + ayon_maya_load = ayon_maya["load"] + load_colors = ayon_maya_load["colors"] for key, color in tuple(load_colors.items()): load_colors[key] = _convert_color(color) + reference_loader = ayon_maya_load["reference_loader"] + reference_loader["namespace"] = ( + reference_loader["namespace"] + .replace("{folder[name]}", "{asset_name}") + .replace("{product[name]}", "{subset}") + ) + output["maya"] = ayon_maya @@ -630,13 +685,21 @@ def _convert_nuke_project_settings(ayon_settings, output): "CreateWriteImage", "CreateWriteRender", ): + create_plugin_settings = ayon_create[creator_name] + create_plugin_settings["temp_rendering_path_template"] = ( + create_plugin_settings["temp_rendering_path_template"] + .replace("{product[name]}", "{subset}") + .replace("{product[type]}", "{family}") + .replace("{task[name]}", "{task}") + .replace("{folder[name]}", "{asset}") + ) new_prenodes = {} - for prenode in ayon_create[creator_name]["prenodes"]: + for prenode in create_plugin_settings["prenodes"]: name = prenode.pop("name") prenode["knobs"] = _convert_nuke_knobs(prenode["knobs"]) new_prenodes[name] = prenode - ayon_create[creator_name]["prenodes"] = new_prenodes + create_plugin_settings["prenodes"] = new_prenodes # --- Publish --- ayon_publish = ayon_nuke["publish"] @@ -651,6 +714,11 @@ def _convert_nuke_project_settings(ayon_settings, output): new_review_data_outputs = {} for item in ayon_publish["ExtractReviewDataMov"]["outputs"]: + item_filter = item["filter"] + if "product_names" in item_filter: + item_filter["subsets"] = item_filter.pop("product_names") + item_filter["families"] = item_filter.pop("product_types") + item["reformat_node_config"] = _convert_nuke_knobs( item["reformat_node_config"]) @@ -659,9 +727,14 @@ def _convert_nuke_project_settings(ayon_settings, output): name = item.pop("name") new_review_data_outputs[name] = item - ayon_publish["ExtractReviewDataMov"]["outputs"] = new_review_data_outputs + collect_instance_data = ayon_publish["CollectInstanceData"] + if "sync_workfile_version_on_product_types" in collect_instance_data: + collect_instance_data["sync_workfile_version_on_families"] = ( + collect_instance_data.pop( + "sync_workfile_version_on_product_types")) + # TODO 'ExtractThumbnail' does not have ideal schema in v3 new_thumbnail_nodes = {} for item in ayon_publish["ExtractThumbnail"]["nodes"]: @@ -707,6 +780,17 @@ def _convert_hiero_project_settings(ayon_settings, output): new_gui_filters[key] = subvalue ayon_hiero["filters"] = new_gui_filters + ayon_load_clip = ayon_hiero["load"]["LoadClip"] + if "product_types" in ayon_load_clip: + ayon_load_clip["families"] = ayon_load_clip.pop("product_types") + + ayon_load_clip = ayon_hiero["load"]["LoadClip"] + ayon_load_clip["clip_name_template"] = ( + ayon_load_clip["clip_name_template"] + .replace("{folder[name]}", "{asset}") + .replace("{product[name]}", "{subset}") + ) + output["hiero"] = ayon_hiero @@ -717,7 +801,14 @@ def _convert_photoshop_project_settings(ayon_settings, output): ayon_photoshop = ayon_settings["photoshop"] _convert_host_imageio(ayon_photoshop) - collect_review = ayon_photoshop["publish"]["CollectReview"] + ayon_publish_photoshop = ayon_photoshop["publish"] + + ayon_colorcoded = ayon_publish_photoshop["CollectColorCodedInstances"] + if "flatten_product_type_template" in ayon_colorcoded: + ayon_colorcoded["flatten_subset_template"] = ( + ayon_colorcoded.pop("flatten_product_type_template")) + + collect_review = ayon_publish_photoshop["CollectReview"] if "active" in collect_review: collect_review["publish"] = collect_review.pop("active") @@ -762,6 +853,14 @@ def _convert_tvpaint_project_settings(ayon_settings, output): extract_sequence_setting["review_bg"] ) + # TODO remove when removed from OpenPype schema + # this is unused setting + ayon_tvpaint["publish"]["ExtractSequence"]["families_to_review"] = [ + "review", + "renderlayer", + "renderscene" + ] + output["tvpaint"] = ayon_tvpaint @@ -776,6 +875,13 @@ def _convert_traypublisher_project_settings(ayon_settings, output): ayon_editorial_simple = ( ayon_traypublisher["editorial_creators"]["editorial_simple"] ) + # Subset -> Product type conversion + if "product_type_presets" in ayon_editorial_simple: + family_presets = ayon_editorial_simple.pop("product_type_presets") + for item in family_presets: + item["family"] = item.pop("product_type") + ayon_editorial_simple["family_presets"] = family_presets + if "shot_metadata_creator" in ayon_editorial_simple: shot_metadata_creator = ayon_editorial_simple.pop( "shot_metadata_creator" @@ -798,6 +904,13 @@ def _convert_traypublisher_project_settings(ayon_settings, output): for item in ayon_editorial_simple["shot_hierarchy"]["parents"]: item["type"] = item.pop("parent_type") + # Simple creators + ayon_simple_creators = ayon_traypublisher["simple_creators"] + for item in ayon_simple_creators: + if "product_type" not in item: + break + item["family"] = item.pop("product_type") + shot_add_tasks = ayon_editorial_simple["shot_add_tasks"] if isinstance(shot_add_tasks, dict): shot_add_tasks = [] @@ -886,6 +999,13 @@ def _convert_kitsu_project_settings(ayon_settings, output): ayon_kitsu_settings = ayon_settings["kitsu"] ayon_kitsu_settings.pop("server") + + integrate_note = ayon_kitsu_settings["publish"]["IntegrateKitsuNote"] + status_change_conditions = integrate_note["status_change_conditions"] + if "product_type_requirements" in status_change_conditions: + status_change_conditions["family_requirements"] = ( + status_change_conditions.pop("product_type_requirements")) + output["kitsu"] = ayon_kitsu_settings @@ -946,12 +1066,25 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): # Publish conversion ayon_publish = ayon_core["publish"] + + ayon_collect_audio = ayon_publish["CollectAudio"] + if "audio_product_name" in ayon_collect_audio: + ayon_collect_audio["audio_subset_name"] = ( + ayon_collect_audio.pop("audio_product_name")) + for profile in ayon_publish["ExtractReview"]["profiles"]: + if "product_types" in profile: + profile["families"] = profile.pop("product_types") new_outputs = {} for output_def in profile.pop("outputs"): name = output_def.pop("name") new_outputs[name] = output_def + output_def_filter = output_def["filter"] + if "product_names" in output_def_filter: + output_def_filter["subsets"] = ( + output_def_filter.pop("product_names")) + for color_key in ("overscan_color", "bg_color"): output_def[color_key] = _convert_color(output_def[color_key]) @@ -967,6 +1100,7 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): profile["outputs"] = new_outputs + # Extract Burnin plugin extract_burnin = ayon_publish["ExtractBurnin"] extract_burnin_options = extract_burnin["options"] for color_key in ("font_color", "bg_color"): @@ -976,16 +1110,56 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): for profile in extract_burnin["profiles"]: extract_burnin_defs = profile["burnins"] + if "product_names" in profile: + profile["subsets"] = profile.pop("product_names") + profile["families"] = profile.pop("product_types") + + for burnin_def in extract_burnin_defs: + for key in ( + "TOP_LEFT", + "TOP_CENTERED", + "TOP_RIGHT", + "BOTTOM_LEFT", + "BOTTOM_CENTERED", + "BOTTOM_RIGHT", + ): + burnin_def[key] = ( + burnin_def[key] + .replace("{product[name]}", "{subset}") + .replace("{Product[name]}", "{Subset}") + .replace("{PRODUCT[NAME]}", "{SUBSET}") + .replace("{product[type]}", "{family}") + .replace("{Product[type]}", "{Family}") + .replace("{PRODUCT[TYPE]}", "{FAMILY}") + .replace("{folder[name]}", "{asset}") + .replace("{Folder[name]}", "{Asset}") + .replace("{FOLDER[NAME]}", "{ASSET}") + ) profile["burnins"] = { extract_burnin_def.pop("name"): extract_burnin_def for extract_burnin_def in extract_burnin_defs } + ayon_integrate_hero = ayon_publish["IntegrateHeroVersion"] + for profile in ayon_integrate_hero["template_name_profiles"]: + if "product_types" not in profile: + break + profile["families"] = profile.pop("product_types") + + if "IntegrateProductGroup" in ayon_publish: + subset_group = ayon_publish.pop("IntegrateProductGroup") + subset_group_profiles = subset_group.pop("product_grouping_profiles") + for profile in subset_group_profiles: + profile["families"] = profile.pop("product_types") + subset_group["subset_grouping_profiles"] = subset_group_profiles + ayon_publish["IntegrateSubsetGroup"] = subset_group + + # Cleanup plugin ayon_cleanup = ayon_publish["CleanUp"] if "patterns" in ayon_cleanup: ayon_cleanup["paterns"] = ayon_cleanup.pop("patterns") - # Project root settings + # Project root settings - json string to dict ayon_core["project_environments"] = json.loads( ayon_core["project_environments"] ) @@ -996,18 +1170,55 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): # Tools settings ayon_tools = ayon_core["tools"] ayon_create_tool = ayon_tools["creator"] + if "product_name_profiles" in ayon_create_tool: + product_name_profiles = ayon_create_tool.pop("product_name_profiles") + for profile in product_name_profiles: + profile["families"] = profile.pop("product_types") + ayon_create_tool["subset_name_profiles"] = product_name_profiles + + for profile in ayon_create_tool["subset_name_profiles"]: + template = profile["template"] + profile["template"] = ( + template + .replace("{task[name]}", "{task}") + .replace("{Task[name]}", "{Task}") + .replace("{TASK[NAME]}", "{TASK}") + .replace("{product[type]}", "{family}") + .replace("{Product[type]}", "{Family}") + .replace("{PRODUCT[TYPE]}", "{FAMILY}") + .replace("{folder[name]}", "{asset}") + .replace("{Folder[name]}", "{Asset}") + .replace("{FOLDER[NAME]}", "{ASSET}") + ) + + product_smart_select_key = "families_smart_select" + if "product_types_smart_select" in ayon_create_tool: + product_smart_select_key = "product_types_smart_select" + new_smart_select_families = { item["name"]: item["task_names"] - for item in ayon_create_tool["families_smart_select"] + for item in ayon_create_tool.pop(product_smart_select_key) } ayon_create_tool["families_smart_select"] = new_smart_select_families ayon_loader_tool = ayon_tools["loader"] - for profile in ayon_loader_tool["family_filter_profiles"]: - if "template_publish_families" in profile: - profile["filter_families"] = ( - profile.pop("template_publish_families") - ) + if "product_type_filter_profiles" in ayon_loader_tool: + product_type_filter_profiles = ( + ayon_loader_tool.pop("product_type_filter_profiles")) + for profile in product_type_filter_profiles: + profile["filter_families"] = profile.pop("filter_product_types") + + ayon_loader_tool["family_filter_profiles"] = ( + product_type_filter_profiles) + + ayon_publish_tool = ayon_tools["publish"] + for profile in ayon_publish_tool["hero_template_name_profiles"]: + if "product_types" in profile: + profile["families"] = profile.pop("product_types") + + for profile in ayon_publish_tool["template_name_profiles"]: + if "product_types" in profile: + profile["families"] = profile.pop("product_types") ayon_core["sync_server"] = ( default_settings["global"]["sync_server"] diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 36e00858ed..b6eb2f52f1 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -53,7 +53,8 @@ }, "ValidateEditorialAssetName": { "enabled": true, - "optional": false + "optional": false, + "active": true }, "ValidateVersion": { "enabled": true, @@ -300,71 +301,6 @@ } ] }, - "IntegrateAssetNew": { - "subset_grouping_profiles": [ - { - "families": [], - "hosts": [], - "task_types": [], - "tasks": [], - "template": "" - } - ], - "template_name_profiles": [ - { - "families": [], - "hosts": [], - "task_types": [], - "tasks": [], - "template_name": "publish" - }, - { - "families": [ - "review", - "render", - "prerender" - ], - "hosts": [], - "task_types": [], - "tasks": [], - "template_name": "render" - }, - { - "families": [ - "simpleUnrealTexture" - ], - "hosts": [ - "standalonepublisher" - ], - "task_types": [], - "tasks": [], - "template_name": "simpleUnrealTexture" - }, - { - "families": [ - "staticMesh", - "skeletalMesh" - ], - "hosts": [ - "maya" - ], - "task_types": [], - "tasks": [], - "template_name": "maya2unreal" - }, - { - "families": [ - "online" - ], - "hosts": [ - "traypublisher" - ], - "task_types": [], - "tasks": [], - "template_name": "online" - } - ] - }, "IntegrateHeroVersion": { "enabled": true, "optional": true, diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 1f4f468656..fdbd6d5d0f 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -60,11 +60,6 @@ 255, 255, 255 - ], - "families_to_review": [ - "review", - "renderlayer", - "renderscene" ] }, "ValidateProjectSettings": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 45fc13bdde..e9255f426e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -273,18 +273,6 @@ "key": "review_bg", "label": "Review BG color", "use_alpha": false - }, - { - "type": "enum", - "key": "families_to_review", - "label": "Families to review", - "multiselection": true, - "enum_items": [ - {"review": "review"}, - {"renderpass": "renderPass"}, - {"renderlayer": "renderLayer"}, - {"renderscene": "renderScene"} - ] } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 81a13d9c57..c7e91fd22d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -118,6 +118,11 @@ "type": "boolean", "key": "optional", "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" } ] }, @@ -888,111 +893,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "IntegrateAssetNew", - "label": "IntegrateAsset (Legacy)", - "is_group": true, - "children": [ - { - "type": "label", - "label": "NOTE: Subset grouping profiles settings were moved to Integrate Subset Group. Please move values there." - }, - { - "type": "list", - "key": "subset_grouping_profiles", - "label": "Subset grouping profiles (DEPRECATED)", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "hosts-enum", - "key": "hosts", - "label": "Hosts", - "multiselection": true - }, - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "tasks", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "template", - "label": "Template" - } - ] - } - }, - { - "type": "label", - "label": "NOTE: Publish template profiles settings were moved to Tools/Publish/Template name profiles. Please move values there." - }, - { - "type": "list", - "key": "template_name_profiles", - "label": "Template name profiles (DEPRECATED)", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "hosts-enum", - "key": "hosts", - "label": "Hosts", - "multiselection": true - }, - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "tasks", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "template_name", - "label": "Template name" - } - ] - } - } - ] - }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 85ec482e73..23fc7c9351 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -320,10 +320,6 @@ "key": "publish", "label": "Publish", "children": [ - { - "type": "label", - "label": "NOTE: For backwards compatibility can be value empty and in that case are used values from IntegrateAssetNew. This will change in future so please move all values here as soon as possible." - }, { "type": "list", "key": "template_name_profiles", diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index 74f75811ff..0ebbd905e5 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -5,6 +5,7 @@ from qtpy import QtWidgets, QtCore, QtGui import qtawesome +from openpype import AYON_SERVER_ENABLED from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS from openpype.tools.utils import ErrorMessageBox @@ -42,10 +43,13 @@ class CreateErrorMessageBox(ErrorMessageBox): def _get_report_data(self): report_message = ( - "Failed to create Subset: \"{subset}\" Family: \"{family}\"" + "Failed to create {subset_label}: \"{subset}\"" + " {family_label}: \"{family}\"" " in Asset: \"{asset}\"" "\n\nError: {message}" ).format( + subset_label="Product" if AYON_SERVER_ENABLED else "Subset", + family_label="Type" if AYON_SERVER_ENABLED else "Family", subset=self._subset_name, family=self._family, asset=self._asset_name, @@ -57,9 +61,13 @@ class CreateErrorMessageBox(ErrorMessageBox): def _create_content(self, content_layout): item_name_template = ( - "Family: {}
" - "Subset: {}
" - "Asset: {}
" + "{}: {{}}
" + "{}: {{}}
" + "{}: {{}}
" + ).format( + "Product type" if AYON_SERVER_ENABLED else "Family", + "Product name" if AYON_SERVER_ENABLED else "Subset", + "Folder" if AYON_SERVER_ENABLED else "Asset" ) exc_msg_template = "{}" @@ -151,15 +159,21 @@ class VariantLineEdit(QtWidgets.QLineEdit): def as_empty(self): self._set_border("empty") - self.report.emit("Empty subset name ..") + self.report.emit("Empty {} name ..".format( + "product" if AYON_SERVER_ENABLED else "subset" + )) def as_exists(self): self._set_border("exists") - self.report.emit("Existing subset, appending next version.") + self.report.emit("Existing {}, appending next version.".format( + "product" if AYON_SERVER_ENABLED else "subset" + )) def as_new(self): self._set_border("new") - self.report.emit("New subset, creating first version.") + self.report.emit("New {}, creating first version.".format( + "product" if AYON_SERVER_ENABLED else "subset" + )) def _set_border(self, status): qcolor, style = self.colors[status] diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 302fe6c366..b305233247 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -223,7 +223,7 @@ class LoaderWindow(QtWidgets.QDialog): lib.schedule(self._refresh, 50, channel="mongo") def on_assetschanged(self, *args): - self.echo("Fetching asset..") + self.echo("Fetching hierarchy..") lib.schedule(self._assetschanged, 50, channel="mongo") def on_subsetschanged(self, *args): diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 1ec695b915..5115f39a69 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -7,6 +7,7 @@ from uuid import uuid4 from qtpy import QtCore, QtGui import qtawesome +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_assets, get_subsets, @@ -143,9 +144,9 @@ class SubsetsModel(BaseRepresentationModel, TreeModel): ] column_labels_mapping = { - "subset": "Subset", - "asset": "Asset", - "family": "Family", + "subset": "Product" if AYON_SERVER_ENABLED else "Subset", + "asset": "Folder" if AYON_SERVER_ENABLED else "Asset", + "family": "Product type" if AYON_SERVER_ENABLED else "Family", "version": "Version", "time": "Time", "author": "Author", diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index a750d8d540..c536f93c9b 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -2,6 +2,7 @@ import collections from qtpy import QtWidgets, QtCore, QtGui +from openpype import AYON_SERVER_ENABLED from openpype.tools.utils import ( PlaceholderLineEdit, RecursiveSortFilterProxyModel, @@ -187,7 +188,8 @@ class AssetsDialog(QtWidgets.QDialog): proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) filter_input = PlaceholderLineEdit(self) - filter_input.setPlaceholderText("Filter assets..") + filter_input.setPlaceholderText("Filter {}..".format( + "folders" if AYON_SERVER_ENABLED else "assets")) asset_view = AssetDialogView(self) asset_view.setModel(proxy_model) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index b7605b1188..1940d16eb8 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -2,6 +2,7 @@ import re from qtpy import QtWidgets, QtCore, QtGui +from openpype import AYON_SERVER_ENABLED from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, PRE_CREATE_THUMBNAIL_KEY, @@ -203,7 +204,9 @@ class CreateWidget(QtWidgets.QWidget): variant_subset_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) variant_subset_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) variant_subset_layout.addRow("Variant", variant_widget) - variant_subset_layout.addRow("Subset", subset_name_input) + variant_subset_layout.addRow( + "Product" if AYON_SERVER_ENABLED else "Subset", + subset_name_input) creator_basics_layout = QtWidgets.QVBoxLayout(creator_basics_widget) creator_basics_layout.setContentsMargins(0, 0, 0, 0) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 0b13f26d57..1bbe73381f 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -9,6 +9,7 @@ import collections from qtpy import QtWidgets, QtCore, QtGui import qtawesome +from openpype import AYON_SERVER_ENABLED from openpype.lib.attribute_definitions import UnknownDef from openpype.tools.attribute_defs import create_widget_for_attr_def from openpype.tools import resources @@ -1116,10 +1117,16 @@ class GlobalAttrsWidget(QtWidgets.QWidget): main_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) main_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) main_layout.addRow("Variant", variant_input) - main_layout.addRow("Asset", asset_value_widget) + main_layout.addRow( + "Folder" if AYON_SERVER_ENABLED else "Asset", + asset_value_widget) main_layout.addRow("Task", task_value_widget) - main_layout.addRow("Family", family_value_widget) - main_layout.addRow("Subset", subset_value_widget) + main_layout.addRow( + "Product type" if AYON_SERVER_ENABLED else "Family", + family_value_widget) + main_layout.addRow( + "Product name" if AYON_SERVER_ENABLED else "Subset", + subset_value_widget) main_layout.addRow(btns_layout) variant_input.value_changed.connect(self._on_variant_change) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 5da25a0c3e..669366dd1d 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -2,6 +2,7 @@ import contextlib from qtpy import QtWidgets, QtCore import qtawesome +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_projects, get_project, @@ -181,7 +182,8 @@ class AssetWidget(QtWidgets.QWidget): filter = PlaceholderLineEdit() filter.textChanged.connect(proxy.setFilterFixedString) - filter.setPlaceholderText("Filter assets..") + filter.setPlaceholderText("Filter {}..".format( + "folders" if AYON_SERVER_ENABLED else "assets")) header.addWidget(filter) header.addWidget(refresh) diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index ffbdd995d6..a45d762c73 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -5,6 +5,7 @@ import qtpy from qtpy import QtWidgets, QtCore, QtGui import qtawesome +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_project, get_assets, @@ -607,7 +608,8 @@ class AssetsWidget(QtWidgets.QWidget): refresh_btn.setToolTip("Refresh items") filter_input = PlaceholderLineEdit(header_widget) - filter_input.setPlaceholderText("Filter assets..") + filter_input.setPlaceholderText("Filter {}..".format( + "folders" if AYON_SERVER_ENABLED else "assets")) # Header header_layout = QtWidgets.QHBoxLayout(header_widget) diff --git a/openpype/vendor/python/common/ayon_api/__init__.py b/openpype/vendor/python/common/ayon_api/__init__.py index ee6672dd38..b1790d1fb6 100644 --- a/openpype/vendor/python/common/ayon_api/__init__.py +++ b/openpype/vendor/python/common/ayon_api/__init__.py @@ -99,22 +99,24 @@ from ._api import ( get_tasks, - get_folder_ids_with_subsets, - get_subset_by_id, - get_subset_by_name, - get_subsets, - get_subset_families, + get_folder_ids_with_products, + get_product_by_id, + get_product_by_name, + get_products, + get_product_types, + get_project_product_types, + get_product_type_names, get_version_by_id, get_version_by_name, version_is_latest, get_versions, - get_hero_version_by_subset_id, + get_hero_version_by_product_id, get_hero_version_by_id, get_hero_versions, get_last_versions, - get_last_version_by_subset_id, - get_last_version_by_subset_name, + get_last_version_by_product_id, + get_last_version_by_product_name, get_representation_by_id, get_representation_by_name, get_representations, @@ -122,6 +124,10 @@ from ._api import ( get_representation_parents, get_repre_ids_by_context_filters, + get_workfiles_info, + get_workfile_info, + get_workfile_info_by_id, + get_thumbnail, get_folder_thumbnail, get_version_thumbnail, @@ -143,8 +149,8 @@ from ._api import ( get_folders_links, get_task_links, get_tasks_links, - get_subset_links, - get_subsets_links, + get_product_links, + get_products_links, get_version_links, get_versions_links, get_representations_links, @@ -251,22 +257,24 @@ __all__ = ( "get_tasks", - "get_folder_ids_with_subsets", - "get_subset_by_id", - "get_subset_by_name", - "get_subsets", - "get_subset_families", + "get_folder_ids_with_products", + "get_product_by_id", + "get_product_by_name", + "get_products", + "get_product_types", + "get_project_product_types", + "get_product_type_names", "get_version_by_id", "get_version_by_name", "version_is_latest", "get_versions", - "get_hero_version_by_subset_id", + "get_hero_version_by_product_id", "get_hero_version_by_id", "get_hero_versions", "get_last_versions", - "get_last_version_by_subset_id", - "get_last_version_by_subset_name", + "get_last_version_by_product_id", + "get_last_version_by_product_name", "get_representation_by_id", "get_representation_by_name", "get_representations", @@ -274,6 +282,10 @@ __all__ = ( "get_representation_parents", "get_repre_ids_by_context_filters", + "get_workfiles_info", + "get_workfile_info", + "get_workfile_info_by_id", + "get_thumbnail", "get_folder_thumbnail", "get_version_thumbnail", @@ -295,8 +307,8 @@ __all__ = ( "get_folders_links", "get_task_links", "get_tasks_links", - "get_subset_links", - "get_subsets_links", + "get_product_links", + "get_products_links", "get_version_links", "get_versions_links", "get_representations_links", diff --git a/openpype/vendor/python/common/ayon_api/_api.py b/openpype/vendor/python/common/ayon_api/_api.py index ed730841ae..f351cd8102 100644 --- a/openpype/vendor/python/common/ayon_api/_api.py +++ b/openpype/vendor/python/common/ayon_api/_api.py @@ -656,29 +656,39 @@ def get_folder_by_name(*args, **kwargs): return con.get_folder_by_name(*args, **kwargs) -def get_folder_ids_with_subsets(*args, **kwargs): +def get_folder_ids_with_products(*args, **kwargs): con = get_server_api_connection() - return con.get_folder_ids_with_subsets(*args, **kwargs) + return con.get_folder_ids_with_products(*args, **kwargs) -def get_subsets(*args, **kwargs): +def get_product_types(*args, **kwargs): con = get_server_api_connection() - return con.get_subsets(*args, **kwargs) + return con.get_product_types(*args, **kwargs) -def get_subset_by_id(*args, **kwargs): +def get_project_product_types(*args, **kwargs): con = get_server_api_connection() - return con.get_subset_by_id(*args, **kwargs) + return con.get_project_product_types(*args, **kwargs) -def get_subset_by_name(*args, **kwargs): +def get_product_type_names(*args, **kwargs): con = get_server_api_connection() - return con.get_subset_by_name(*args, **kwargs) + return con.get_product_type_names(*args, **kwargs) -def get_subset_families(*args, **kwargs): +def get_products(*args, **kwargs): con = get_server_api_connection() - return con.get_subset_families(*args, **kwargs) + return con.get_products(*args, **kwargs) + + +def get_product_by_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_product_by_id(*args, **kwargs) + + +def get_product_by_name(*args, **kwargs): + con = get_server_api_connection() + return con.get_product_by_name(*args, **kwargs) def get_versions(*args, **kwargs): @@ -701,9 +711,9 @@ def get_hero_version_by_id(*args, **kwargs): return con.get_hero_version_by_id(*args, **kwargs) -def get_hero_version_by_subset_id(*args, **kwargs): +def get_hero_version_by_product_id(*args, **kwargs): con = get_server_api_connection() - return con.get_hero_version_by_subset_id(*args, **kwargs) + return con.get_hero_version_by_product_id(*args, **kwargs) def get_hero_versions(*args, **kwargs): @@ -716,14 +726,14 @@ def get_last_versions(*args, **kwargs): return con.get_last_versions(*args, **kwargs) -def get_last_version_by_subset_id(*args, **kwargs): +def get_last_version_by_product_id(*args, **kwargs): con = get_server_api_connection() - return con.get_last_version_by_subset_id(*args, **kwargs) + return con.get_last_version_by_product_id(*args, **kwargs) -def get_last_version_by_subset_name(*args, **kwargs): +def get_last_version_by_product_name(*args, **kwargs): con = get_server_api_connection() - return con.get_last_version_by_subset_name(*args, **kwargs) + return con.get_last_version_by_product_name(*args, **kwargs) def version_is_latest(*args, **kwargs): @@ -761,6 +771,21 @@ def get_repre_ids_by_context_filters(*args, **kwargs): return con.get_repre_ids_by_context_filters(*args, **kwargs) +def get_workfiles_info(*args, **kwargs): + con = get_server_api_connection() + return con.get_workfiles_info(*args, **kwargs) + + +def get_workfile_info(*args, **kwargs): + con = get_server_api_connection() + return con.get_workfile_info(*args, **kwargs) + + +def get_workfile_info_by_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_workfile_info_by_id(*args, **kwargs) + + def create_project( project_name, project_code, @@ -954,31 +979,31 @@ def get_task_links( ) -def get_subsets_links( +def get_products_links( project_name, - subset_ids=None, + product_ids=None, link_types=None, link_direction=None ): con = get_server_api_connection() - return con.get_subsets_links( + return con.get_products_links( project_name, - subset_ids, + product_ids, link_types, link_direction ) -def get_subset_links( +def get_product_links( project_name, - subset_id, + product_id, link_types=None, link_direction=None ): con = get_server_api_connection() - return con.get_subset_links( + return con.get_product_links( project_name, - subset_id, + product_id, link_types, link_direction ) diff --git a/openpype/vendor/python/common/ayon_api/constants.py b/openpype/vendor/python/common/ayon_api/constants.py index 03451756a0..e2b05a5cae 100644 --- a/openpype/vendor/python/common/ayon_api/constants.py +++ b/openpype/vendor/python/common/ayon_api/constants.py @@ -4,6 +4,13 @@ SERVER_API_ENV_KEY = "AYON_API_KEY" # Backwards compatibility SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY +# --- Product types --- +DEFAULT_PRODUCT_TYPE_FIELDS = { + "name", + "icon", + "color", +} + # --- Project --- DEFAULT_PROJECT_FIELDS = { "active", @@ -36,13 +43,13 @@ DEFAULT_TASK_FIELDS = { "assignees", } -# --- Subsets --- -DEFAULT_SUBSET_FIELDS = { +# --- Products --- +DEFAULT_PRODUCT_FIELDS = { "id", "name", "folderId", "active", - "family", + "productType", } # --- Versions --- @@ -50,7 +57,7 @@ DEFAULT_VERSION_FIELDS = { "id", "name", "version", - "subsetId", + "productId", "taskId", "active", "author", diff --git a/openpype/vendor/python/common/ayon_api/entity_hub.py b/openpype/vendor/python/common/ayon_api/entity_hub.py index c6baceeb31..d71ce18839 100644 --- a/openpype/vendor/python/common/ayon_api/entity_hub.py +++ b/openpype/vendor/python/common/ayon_api/entity_hub.py @@ -558,7 +558,7 @@ class EntityHub(object): folder_fields = set( self._connection.get_default_fields_for_type("folder") ) - folder_fields.add("hasSubsets") + folder_fields.add("hasProducts") if self._allow_data_changes: folder_fields.add("data") return folder_fields @@ -602,7 +602,7 @@ class EntityHub(object): for folder in folders_by_parent_id[parent_id]: folder_entity = self.add_folder(folder) children_ids.add(folder_entity.id) - folder_entity.has_published_content = folder["hasSubsets"] + folder_entity.has_published_content = folder["hasProducts"] hierarchy_queue.append((folder_entity.id, folder_entity)) for task in tasks_by_parent_id[parent_id]: @@ -1564,7 +1564,7 @@ class FolderEntity(BaseEntity): self._orig_folder_type = folder_type self._orig_label = label - # Know if folder has any subsets + # Know if folder has any products # - is used to know if folder allows hierarchy changes self._has_published_content = False self._path = path diff --git a/openpype/vendor/python/common/ayon_api/graphql_queries.py b/openpype/vendor/python/common/ayon_api/graphql_queries.py index 1fc653cf68..4af8c53e4e 100644 --- a/openpype/vendor/python/common/ayon_api/graphql_queries.py +++ b/openpype/vendor/python/common/ayon_api/graphql_queries.py @@ -123,6 +123,51 @@ def projects_graphql_query(fields): return query +def product_types_query(fields): + query = GraphQlQuery("ProductTypes") + product_types_field = query.add_field("productTypes") + + nested_fields = fields_to_dict(fields) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, product_types_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 + +def project_product_types_query(fields): + query = GraphQlQuery("ProjectProductTypes") + project_query = query.add_field("project") + project_name_var = query.add_variable("projectName", "String!") + project_query.set_filter("name", project_name_var) + product_types_field = project_query.add_field("productTypes") + nested_fields = fields_to_dict(fields) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, product_types_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 + + def folders_graphql_query(fields): query = GraphQlQuery("FoldersQuery") project_name_var = query.add_variable("projectName", "String!") @@ -130,7 +175,7 @@ def folders_graphql_query(fields): parent_folder_ids_var = query.add_variable("parentFolderIds", "[String!]") folder_paths_var = query.add_variable("folderPaths", "[String!]") folder_names_var = query.add_variable("folderNames", "[String!]") - has_subsets_var = query.add_variable("folderHasSubsets", "Boolean!") + has_products_var = query.add_variable("folderHasProducts", "Boolean!") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) @@ -140,7 +185,7 @@ def folders_graphql_query(fields): folders_field.set_filter("parentIds", parent_folder_ids_var) folders_field.set_filter("names", folder_names_var) folders_field.set_filter("paths", folder_paths_var) - folders_field.set_filter("hasSubsets", has_subsets_var) + folders_field.set_filter("hasProducts", has_products_var) nested_fields = fields_to_dict(fields) add_links_fields(folders_field, nested_fields) @@ -198,28 +243,28 @@ def tasks_graphql_query(fields): return query -def subsets_graphql_query(fields): - query = GraphQlQuery("SubsetsQuery") +def products_graphql_query(fields): + query = GraphQlQuery("ProductsQuery") project_name_var = query.add_variable("projectName", "String!") folder_ids_var = query.add_variable("folderIds", "[String!]") - subset_ids_var = query.add_variable("subsetIds", "[String!]") - subset_names_var = query.add_variable("subsetNames", "[String!]") + product_ids_var = query.add_variable("productIds", "[String!]") + product_names_var = query.add_variable("productNames", "[String!]") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - subsets_field = project_field.add_field_with_edges("subsets") - subsets_field.set_filter("ids", subset_ids_var) - subsets_field.set_filter("names", subset_names_var) - subsets_field.set_filter("folderIds", folder_ids_var) + products_field = project_field.add_field_with_edges("products") + products_field.set_filter("ids", product_ids_var) + products_field.set_filter("names", product_names_var) + products_field.set_filter("folderIds", folder_ids_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields(subsets_field, nested_fields) + add_links_fields(products_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): - query_queue.append((key, value, subsets_field)) + query_queue.append((key, value, products_field)) while query_queue: item = query_queue.popleft() @@ -237,7 +282,7 @@ def versions_graphql_query(fields): query = GraphQlQuery("VersionsQuery") project_name_var = query.add_variable("projectName", "String!") - subset_ids_var = query.add_variable("subsetIds", "[String!]") + product_ids_var = query.add_variable("productIds", "[String!]") version_ids_var = query.add_variable("versionIds", "[String!]") versions_var = query.add_variable("versions", "[Int!]") hero_only_var = query.add_variable("heroOnly", "Boolean") @@ -249,20 +294,20 @@ def versions_graphql_query(fields): project_field = query.add_field("project") project_field.set_filter("name", project_name_var) - subsets_field = project_field.add_field_with_edges("versions") - subsets_field.set_filter("ids", version_ids_var) - subsets_field.set_filter("subsetIds", subset_ids_var) - subsets_field.set_filter("versions", versions_var) - subsets_field.set_filter("heroOnly", hero_only_var) - subsets_field.set_filter("latestOnly", latest_only_var) - subsets_field.set_filter("heroOrLatestOnly", hero_or_latest_only_var) + products_field = project_field.add_field_with_edges("versions") + products_field.set_filter("ids", version_ids_var) + products_field.set_filter("productIds", product_ids_var) + products_field.set_filter("versions", versions_var) + products_field.set_filter("heroOnly", hero_only_var) + products_field.set_filter("latestOnly", latest_only_var) + products_field.set_filter("heroOrLatestOnly", hero_or_latest_only_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields(subsets_field, nested_fields) + add_links_fields(products_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): - query_queue.append((key, value, subsets_field)) + query_queue.append((key, value, products_field)) while query_queue: item = query_queue.popleft() @@ -312,7 +357,7 @@ def representations_graphql_query(fields): def representations_parents_qraphql_query( - version_fields, subset_fields, folder_fields + version_fields, product_fields, folder_fields ): query = GraphQlQuery("RepresentationsParentsQuery") @@ -331,11 +376,11 @@ def representations_parents_qraphql_query( for key, value in fields_to_dict(version_fields).items(): fields_queue.append((key, value, version_field)) - subset_field = version_field.add_field("subset") - for key, value in fields_to_dict(subset_fields).items(): - fields_queue.append((key, value, subset_field)) + product_field = version_field.add_field("product") + for key, value in fields_to_dict(product_fields).items(): + fields_queue.append((key, value, product_field)) - folder_field = subset_field.add_field("folder") + folder_field = product_field.add_field("folder") for key, value in fields_to_dict(folder_fields).items(): fields_queue.append((key, value, folder_field)) diff --git a/openpype/vendor/python/common/ayon_api/operations.py b/openpype/vendor/python/common/ayon_api/operations.py index b5689de7c0..7cf610a566 100644 --- a/openpype/vendor/python/common/ayon_api/operations.py +++ b/openpype/vendor/python/common/ayon_api/operations.py @@ -31,12 +31,14 @@ def new_folder_entity( Args: name (str): Is considered as unique identifier of folder in project. - parent_id (str): Id of parent folder. - attribs (Dict[str, Any]): Explicitly set attributes of folder. - data (Dict[str, Any]): Custom folder data. Empty dictionary is used - if not passed. - thumbnail_id (str): Id of thumbnail related to folder. - entity_id (str): Predefined id of entity. New id is + folder_type (str): Type of folder. + parent_id (Optional[str]]): Id of parent folder. + 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. + entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: @@ -64,23 +66,32 @@ def new_folder_entity( } -def new_subset_entity( - name, family, folder_id, attribs=None, data=None, entity_id=None +def new_product_entity( + name, + product_type, + folder_id, + status=None, + attribs=None, + data=None, + entity_id=None ): - """Create skeleton data of subset entity. + """Create skeleton data of product entity. Args: - name (str): Is considered as unique identifier of subset under folder. - family (str): Subset's family. + name (str): Is considered as unique identifier of + product under folder. + product_type (str): Product type. folder_id (str): Id of parent folder. - attribs (Dict[str, Any]): Explicitly set attributes of subset. - data (Dict[str, Any]): Subset entity data. Empty dictionary is used - if not passed. Value of 'family' is used to fill 'family'. - entity_id (str): Predefined id of entity. New id is + status (Optional[str]): Product status. + attribs (Optional[Dict[str, Any]]): Explicitly set attributes + of product. + data (Optional[Dict[str, Any]]): product entity data. Empty dictionary + is used if not passed. + entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: - Dict[str, Any]: Skeleton of subset entity. + Dict[str, Any]: Skeleton of product entity. """ if attribs is None: @@ -89,19 +100,22 @@ def new_subset_entity( if data is None: data = {} - return { + output = { "id": _create_or_convert_to_id(entity_id), "name": name, - "family": family, + "productType": product_type, "attrib": attribs, "data": data, - "folderId": _create_or_convert_to_id(folder_id) + "folderId": _create_or_convert_to_id(folder_id), } + if status: + output["status"] = status + return output def new_version_entity( version, - subset_id, + product_id, task_id=None, thumbnail_id=None, author=None, @@ -113,14 +127,15 @@ def new_version_entity( Args: version (int): Is considered as unique identifier of version - under subset. - subset_id (str): Id of parent subset. - task_id (str): Id of task under which subset was created. - thumbnail_id (str): Thumbnail related to version. - author (str): Name of version author. - attribs (Dict[str, Any]): Explicitly set attributes of version. - data (Dict[str, Any]): Version entity custom data. - entity_id (str): Predefined id of entity. New id is + 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. + attribs (Optional[Dict[str, Any]]): Explicitly set attributes + of version. + data (Optional[Dict[str, Any]]): Version entity custom data. + entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: @@ -139,7 +154,7 @@ def new_version_entity( output = { "id": _create_or_convert_to_id(entity_id), "version": int(version), - "subsetId": _create_or_convert_to_id(subset_id), + "productId": _create_or_convert_to_id(product_id), "attrib": attribs, "data": data } @@ -154,7 +169,7 @@ def new_version_entity( def new_hero_version_entity( version, - subset_id, + product_id, task_id=None, thumbnail_id=None, author=None, @@ -166,14 +181,15 @@ def new_hero_version_entity( Args: version (int): Is considered as unique identifier of version - under subset. Should be same as standard version if there is any. - subset_id (str): Id of parent subset. - task_id (str): Id of task under which subset was created. - thumbnail_id (str): Thumbnail related to version. - author (str): Name of version author. - attribs (Dict[str, Any]): Explicitly set attributes of version. - data (Dict[str, Any]): Version entity data. - entity_id (str): Predefined id of entity. New id is + 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. + thumbnail_id (Optional[str]): Thumbnail related to version. + author (Optional[str]): Name of version author. + attribs (Optional[Dict[str, Any]]): Explicitly set attributes + of version. + data (Optional[Dict[str, Any]]): Version entity data. + entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: @@ -189,7 +205,7 @@ def new_hero_version_entity( output = { "id": _create_or_convert_to_id(entity_id), "version": -abs(int(version)), - "subsetId": subset_id, + "productId": product_id, "attrib": attribs, "data": data } @@ -211,9 +227,10 @@ def new_representation_entity( name (str): Representation name considered as unique identifier of representation under version. version_id (str): Id of parent version. - attribs (Dict[str, Any]): Explicitly set attributes of representation. - data (Dict[str, Any]): Representation entity data. - entity_id (str): Predefined id of entity. New id is created + attribs (Optional[Dict[str, Any]]): Explicitly set attributes + of representation. + data (Optional[Dict[str, Any]]): Representation entity data. + entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: @@ -247,8 +264,8 @@ def new_workfile_info_doc( 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. - data (Dict[str, Any]): Additional metadata. - entity_id (str): Predefined id of entity. New id is created + data (Optional[Dict[str, Any]]): Additional metadata. + entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index 675f5ea4be..253d590658 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -17,10 +17,11 @@ import requests from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError from .constants import ( + DEFAULT_PRODUCT_TYPE_FIELDS, DEFAULT_PROJECT_FIELDS, DEFAULT_FOLDER_FIELDS, DEFAULT_TASK_FIELDS, - DEFAULT_SUBSET_FIELDS, + DEFAULT_PRODUCT_FIELDS, DEFAULT_VERSION_FIELDS, DEFAULT_REPRESENTATION_FIELDS, REPRESENTATION_FILES_FIELDS, @@ -32,9 +33,11 @@ from .graphql import GraphQlQuery, INTROSPECTION_QUERY from .graphql_queries import ( project_graphql_query, projects_graphql_query, + project_product_types_query, + product_types_query, folders_graphql_query, tasks_graphql_query, - subsets_graphql_query, + products_graphql_query, versions_graphql_query, representations_graphql_query, representations_parents_qraphql_query, @@ -293,12 +296,12 @@ class ServerAPI(object): Args: base_url (str): Example: http://localhost:5000 - token (str): Access token (api key) to server. - site_id (str): Unique name of site. Should be the same when + token (Optional[str]): Access token (api key) to server. + site_id (Optional[str]): Unique name of site. Should be the same when connection is created from the same machine under same user. - client_version (str): Version of client application (used in + client_version (Optional[str]): Version of client application (used in desktop client application). - default_settings_variant (Union[str, None]): Settings variant used by + default_settings_variant (Optional[str]): Settings variant used by default if a method for settings won't get any (by default is 'production'). """ @@ -317,7 +320,7 @@ class ServerAPI(object): base_url = base_url.rstrip("/") self._base_url = base_url self._rest_url = "{}/api".format(base_url) - self._graphl_url = "{}/graphql".format(base_url) + self._graphql_url = "{}/graphql".format(base_url) self._log = None self._access_token = token self._site_id = site_id @@ -472,7 +475,7 @@ class ServerAPI(object): 'as_user' context manager is not entered. Args: - username (Union[str, None]): Username to work as when service. + username (Optional[str]): Username to work as when service. Raises: ValueError: When connection is not yet authenticated or api key @@ -888,13 +891,14 @@ class ServerAPI(object): Not all event happen on a project. Args: - topics (Iterable[str]): Name of topics. - project_names (Iterable[str]): Project on which event happened. - states (Iterable[str]): Filtering by states. - users (Iterable[str]): Filtering by users who created/triggered - an event. - include_logs (bool): Query also log events. - fields (Union[Iterable[str], None]): Fields that should be received + topics (Optional[Iterable[str]]): Name of topics. + project_names (Optional[Iterable[str]]): Project on which + event happened. + states (Optional[Iterable[str]]): Filtering by states. + users (Optional[Iterable[str]]): Filtering by users + who created/triggered an event. + include_logs (Optional[bool]): Query also log events. + fields (Optional[Iterable[str]]): Fields that should be received for each event. Returns: @@ -951,17 +955,18 @@ class ServerAPI(object): summary=None, payload=None ): - kwargs = {} - for key, value in ( - ("sender", sender), - ("projectName", project_name), - ("status", status), - ("description", description), - ("summary", summary), - ("payload", payload), - ): - if value is not None: - kwargs[key] = value + kwargs = { + key: value + for key, value in ( + ("sender", sender), + ("project", project_name), + ("status", status), + ("description", description), + ("summary", summary), + ("payload", payload), + ) + if value is not None + } response = self.patch( "events/{}".format(event_id), **kwargs @@ -990,15 +995,16 @@ class ServerAPI(object): hash (Optional[str]): Event hash. project_name (Optional[str]): Project name. username (Optional[str]): Username which triggered event. - dependencies (Optional[list[str]]): List of event id deprendencies. + dependencies (Optional[list[str]]): List of event id dependencies. description (Optional[str]): Description of event. summary (Optional[dict[str, Any]]): Summary of event that can be used for simple filtering on listeners. payload (Optional[dict[str, Any]]): Full payload of event data with all details. - finished (bool): Mark event as finished on dispatch. - store (bool): Store event in event queue for possible future processing - otherwise is event send only to active listeners. + finished (Optional[bool]): Mark event as finished on dispatch. + store (Optional[bool]): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. """ if summary is None: @@ -1053,14 +1059,14 @@ class ServerAPI(object): Use-case: - Service 1 is creating events with topic 'my.leech' - Service 2 process 'my.leech' and uses target topic 'my.process' - - this service can run on 1..n machines + - this service can run on 1-n machines - all events must be processed in a sequence by their creation time and only one event can be processed at a time - in this case 'sequential' should be set to 'True' so only one machine is actually processing events, but if one goes down there are other that can take place - Service 3 process 'my.leech' and uses target topic 'my.discover' - - this service can run on 1..n machines + - this service can run on 1-n machines - order of events is not important - 'sequential' should be 'False' @@ -1068,8 +1074,10 @@ class ServerAPI(object): source_topic (str): Source topic to enroll. target_topic (str): Topic of dependent event. sender (str): Identifier of sender (e.g. service name or username). - description (str): Human readable text shown in target event. - sequential (bool): The source topic must be processed in sequence. + description (Optional[str]): Human readable text shown + in target event. + sequential (Optional[bool]): The source topic must be processed + in sequence. Returns: Union[None, dict[str, Any]]: None if there is no event matching @@ -1114,7 +1122,9 @@ class ServerAPI(object): f_stream.write(chunk) progress.add_transferred_chunk(len(chunk)) - def download_file(self, endpoint, filepath, chunk_size=None, progress=None): + def download_file( + self, endpoint, filepath, chunk_size=None, progress=None + ): """Download file from AYON server. Endpoint can be full url (must start with 'base_url' of api object). @@ -1126,9 +1136,10 @@ class ServerAPI(object): Args: endpoint (str): Endpoint or URL to file that should be downloaded. filepath (str): Path where file will be downloaded. - chunk_size (int): Size of chunks that are received in single loop. - progress (TransferProgress): Object that gives ability to track - download progress. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. """ if not chunk_size: @@ -1186,8 +1197,8 @@ class ServerAPI(object): Args: endpoint (str): Endpoint or url where file will be uploaded. filepath (str): Source filepath. - progress (TransferProgress): Object that gives ability to track - upload progress. + progress (Optional[TransferProgress]): Object that gives ability + to track upload progress. """ if endpoint.startswith(self._base_url): @@ -1232,7 +1243,7 @@ class ServerAPI(object): Args: query (str): GraphQl query string. - variables (Union[None, dict[str, Any]): Variables that can be + variables (Optional[dict[str, Any]): Variables that can be used in query. Returns: @@ -1242,7 +1253,7 @@ class ServerAPI(object): data = {"query": query, "variables": variables or {}} response = self._do_rest_request( RequestTypes.post, - self._graphl_url, + self._graphql_url, json=data ) response.raise_for_status() @@ -1271,7 +1282,7 @@ class ServerAPI(object): """Get components schema. Name of components does not match entity type names e.g. 'project' is - under 'ProjectModel'. We should find out some mapping. Also there + under 'ProjectModel'. We should find out some mapping. Also, there are properties which don't have information about reference to object e.g. 'config' has just object definition without reference schema. @@ -1432,8 +1443,8 @@ class ServerAPI(object): for attr in attributes } - if entity_type == "subset": - return DEFAULT_SUBSET_FIELDS | { + if entity_type == "product": + return DEFAULT_PRODUCT_FIELDS | { "attrib.{}".format(attr) for attr in attributes } @@ -1454,14 +1465,17 @@ class ServerAPI(object): } ) + if entity_type == "productType": + return DEFAULT_PRODUCT_TYPE_FIELDS + raise ValueError("Unknown entity type \"{}\"".format(entity_type)) def get_addons_info(self, details=True): """Get information about addons available on server. Args: - details (bool): Detailed data with information how to get client - code. + details (Optional[bool]): Detailed data with information how + to get client code. """ endpoint = "addons" @@ -1520,11 +1534,11 @@ class ServerAPI(object): addon_version (str): Addon version. filename (str): Filename in private folder on server. destination_dir (str): Where the file should be downloaded. - destination_filename (str): Name of destination filename. Source - filename is used if not passed. - chunk_size (int): Download chunk size. - progress (TransferProgress): Object that gives ability to track - download progress. + destination_filename (Optional[str]): Name of destination + filename. Source filename is used if not passed. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. Returns: str: Filepath to downloaded file. @@ -1586,7 +1600,7 @@ class ServerAPI(object): python_modules=None, sources=None ): - """Update or create dependency package infor by it's identifiers. + """Update or create dependency package for identifiers. The endpoint can be used to create or update dependency package. @@ -1595,15 +1609,17 @@ class ServerAPI(object): platform_name (Literal["windows", "linux", "darwin"]): Platform for which is dependency package targeted. size (int): Size of dependency package in bytes. - checksum (str): Checksum of archive file where dependecies are. - checksum_algorithm (str): Algorithm used to calculate checksum. - By default, is used 'md5' (defined by server). - supported_addons (Dict[str, str]): Name of addons for which was the - package created ('{"": "", ...}'). - python_modules (Dict[str, str]): Python modules in dependencies - package ('{"": "", ...}'). - sources (List[Dict[str, Any]]): Information about sources where - dependency package is available. + checksum (str): Checksum of archive file where dependencies are. + checksum_algorithm (Optional[str]): Algorithm used to calculate + checksum. By default, is used 'md5' (defined by server). + supported_addons (Optional[Dict[str, str]]): Name of addons for + which was the package created. + '{"": "", ...}' + python_modules (Optional[Dict[str, str]]): Python modules in + dependencies package. + '{"": "", ...}' + sources (Optional[List[Dict[str, Any]]]): Information about + sources where dependency package is available. """ kwargs = { @@ -1625,7 +1641,7 @@ class ServerAPI(object): checksum=checksum, **kwargs ) - if response.status not in (200, 201): + if response.status not in (200, 201, 204): raise ServerError("Failed to create/update dependency") return response.data @@ -1647,11 +1663,12 @@ class ServerAPI(object): package_name (str): Name of package to download. dst_directory (str): Where the file should be downloaded. filename (str): Name of destination filename. - platform_name (str): Name of platform for which the dependency - package is targetter. Default value is current platform. - chunk_size (int): Download chunk size. - progress (TransferProgress): Object that gives ability to track - download progress. + platform_name (Optional[str]): Name of platform for which the + dependency package is targeted. Default value is + current platform. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. Returns: str: Filepath to downloaded file. @@ -1676,7 +1693,8 @@ class ServerAPI(object): Args: filepath (str): Path to a package file. package_name (str): Name of package. Must be unique. - platform_name (str): For which platform is the package targeted. + platform_name (Optional[str]): For which platform is the + package targeted. progress (Optional[TransferProgress]): Object to keep track about upload state. """ @@ -1695,8 +1713,8 @@ class ServerAPI(object): Args: package_name (str): Name of package to remove. - platform_name (Optional[str]): Which platform of the package should - be removed. Current platform is used if not passed. + platform_name (Optional[str]): Which platform of the package + should be removed. Current platform is used if not passed. """ if platform_name is None: @@ -1741,7 +1759,7 @@ class ServerAPI(object): if preset name is set to 'None'. Args: - Union[str, None]: Preset name. + preset_name (Optional[str]): Preset name. Returns: dict[str, Any]: Anatomy preset values. @@ -1807,7 +1825,7 @@ class ServerAPI(object): Args: addon_name (str): Name of addon. addon_version (str): Version of addon. - project_name (Union[str, None]): Schema for specific project or + project_name (Optional[str]): Schema for specific project or default studio schemas. Returns: @@ -1855,8 +1873,8 @@ class ServerAPI(object): Args: addon_name (str): Name of addon. addon_version (str): Version of addon. - variant (str): Name of settings variant. By default, is used - 'default_settings_variant' passed on init. + variant (Optional[str]): Name of settings variant. By default, + is used 'default_settings_variant' passed on init. Returns: dict[str, Any]: Addon settings. @@ -1898,13 +1916,14 @@ class ServerAPI(object): addon_version (str): Version of addon. project_name (str): Name of project for which the settings are received. - variant (str): Name of settings variant. By default, is used - 'production'. - site_id (str): Name of site which is used for site overrides. Is - filled with connection 'site_id' attribute if not passed. - use_site (bool): To force disable option of using site overrides - set to 'False'. In that case won't be applied any site - overrides. + variant (Optional[str]): Name of settings variant. By default, + is used 'production'. + site_id (Optional[str]): Name of site which is used for site + overrides. Is filled with connection 'site_id' attribute + if not passed. + use_site (Optional[bool]): To force disable option of using site + overrides set to 'False'. In that case won't be applied + any site overrides. Returns: dict[str, Any]: Addon settings. @@ -1951,15 +1970,17 @@ class ServerAPI(object): Args: addon_name (str): Name of addon. addon_version (str): Version of addon. - project_name (str): Name of project for which the settings are - received. A studio settings values are received if is 'None'. - variant (str): Name of settings variant. By default, is used - 'production'. - site_id (str): Name of site which is used for site overrides. Is - filled with connection 'site_id' attribute if not passed. - use_site (bool): To force disable option of using site overrides - set to 'False'. In that case won't be applied any site - overrides. + project_name (Optional[str]): Name of project for which the + settings are received. A studio settings values are received + if is 'None'. + variant (Optional[str]): Name of settings variant. By default, + is used 'production'. + site_id (Optional[str]): Name of site which is used for site + overrides. Is filled with connection 'site_id' attribute + if not passed. + use_site (Optional[bool]): To force disable option of using + site overrides set to 'False'. In that case won't be applied + any site overrides. Returns: dict[str, Any]: Addon settings. @@ -1983,8 +2004,8 @@ class ServerAPI(object): Args: addon_name (str): Name of addon. addon_version (str): Version of addon. - site_id (str): Name of site for which should be settings returned. - using 'site_id' attribute if not passed. + site_id (Optional[str]): Name of site for which should be settings + returned. using 'site_id' attribute if not passed. Returns: dict[str, Any]: Site settings. @@ -2007,8 +2028,8 @@ class ServerAPI(object): """All addons settings in one bulk. Args: - variant (Literal[production, staging]): Variant of settings. By - default, is used 'production'. + variant (Optional[Literal[production, staging]]): Variant of + settings. By default, is used 'production'. only_values (Optional[bool]): Output will contain only settings values without metadata about addons. @@ -2105,9 +2126,9 @@ class ServerAPI(object): By default, is used 'production'. site_id (Optional[str]): Id of site for which want to receive site overrides. - use_site (bool): To force disable option of using site overrides - set to 'False'. In that case won't be applied any site - overrides. + use_site (Optional[bool]): To force disable option of using site + overrides set to 'False'. In that case won't be applied + any site overrides. only_values (Optional[bool]): Only settings values will be returned. By default, is set to 'True'. """ @@ -2147,10 +2168,10 @@ class ServerAPI(object): User must be logged in. Args: - active (Union[bool, None]): Filter active/inactive projects. Both + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. - library (bool): Filter standard/library projects. Both are - returned if 'None' is passed. Returns: Generator[Dict[str, Any]]: Available projects. @@ -2166,7 +2187,7 @@ class ServerAPI(object): Args: project_name (str): Name of project where entity is. - entity_type (Literal["folder", "task", "subset", "version"]): The + entity_type (Literal["folder", "task", "product", "version"]): The entity type which should be received. entity_id (str): Id of entity. @@ -2191,8 +2212,8 @@ class ServerAPI(object): def get_rest_task(self, project_name, task_id): return self.get_rest_entity_by_id(project_name, "task", task_id) - def get_rest_subset(self, project_name, subset_id): - return self.get_rest_entity_by_id(project_name, "subset", subset_id) + def get_rest_product(self, project_name, product_id): + return self.get_rest_entity_by_id(project_name, "product", product_id) def get_rest_version(self, project_name, version_id): return self.get_rest_entity_by_id(project_name, "version", version_id) @@ -2208,10 +2229,10 @@ class ServerAPI(object): User must be logged in. Args: - active (Union[bool, None[): Filter active/inactive projects. Both + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. - library (bool): Filter standard/library projects. Both are - returned if 'None' is passed. Returns: List[str]: List of available project names. @@ -2222,7 +2243,7 @@ class ServerAPI(object): query_keys["active"] = "true" if active else "false" if library is not None: - query_keys["library"] = "true" if active else "false" + query_keys["library"] = "true" if library else "false" query = "" if query_keys: query = "?{}".format(",".join([ @@ -2245,14 +2266,14 @@ class ServerAPI(object): """Get projects. Args: - active (Union[bool, None]): Filter active or inactive projects. + active (Optional[bool]): Filter active or inactive projects. Filter is disabled when 'None' is passed. - library (Union[bool, None]): Filter library projects. Filter is + library (Optional[bool]): Filter library projects. Filter is disabled when 'None' is passed. - fields (Union[Iterable[str], None]): fields to be queried + fields (Optional[Iterable[str]]): fields to be queried for project. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Generator[Dict[str, Any]]: Queried projects. @@ -2289,10 +2310,10 @@ class ServerAPI(object): Args: project_name (str): Name of project. - fields (Union[Iterable[str], None]): fields to be queried + fields (Optional[Iterable[str]]): fields to be queried for project. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[Dict[str, Any], None]: Project entity data or None @@ -2345,7 +2366,7 @@ class ServerAPI(object): """Query folders from server. Todos: - Folder name won't be unique identifier so we should add folder path + Folder name won't be unique identifier, so we should add folder path filtering. Notes: @@ -2353,18 +2374,20 @@ class ServerAPI(object): Args: project_name (str): Name of project. - folder_ids (Iterable[str]): Folder ids to filter. - folder_paths (Iterable[str]): Folder paths used for filtering. - folder_names (Iterable[str]): Folder names used for filtering. - parent_ids (Iterable[str]): Ids of folder parents. Use 'None' - if folder is direct child of project. - active (Union[bool, None]): Filter active/inactive folders. + folder_ids (Optional[Iterable[str]]): Folder ids to filter. + folder_paths (Optional[Iterable[str]]): Folder paths used + for filtering. + folder_names (Optional[Iterable[str]]): Folder names used + for filtering. + parent_ids (Optional[Iterable[str]]): Ids of folder parents. + Use 'None' if folder is direct child of project. + active (Optional[bool]): Filter active/inactive folders. Both are returned if is set to None. - fields (Union[Iterable[str], None]): Fields to be queried for + fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Generator[dict[str, Any]]: Queried folder entities. @@ -2459,10 +2482,10 @@ class ServerAPI(object): project_name (str): Name of project where to look for queried entities. folder_id (str): Folder id. - fields (Union[Iterable[str], None]): Fields that should be returned. + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Folder entity data or None if was not found. @@ -2494,10 +2517,10 @@ class ServerAPI(object): project_name (str): Name of project where to look for queried entities. folder_path (str): Folder path. - fields (Union[Iterable[str], None]): Fields that should be returned. + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Folder entity data or None if was not found. @@ -2531,10 +2554,10 @@ class ServerAPI(object): project_name (str): Name of project where to look for queried entities. folder_name (str): Folder name. - fields (Union[Iterable[str], None]): Fields that should be returned. + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Folder entity data or None if was not found. @@ -2551,21 +2574,21 @@ class ServerAPI(object): return folder return None - def get_folder_ids_with_subsets(self, project_name, folder_ids=None): - """Find folders which have at least one subset. + def get_folder_ids_with_products(self, project_name, folder_ids=None): + """Find folders which have at least one product. - Folders that have at least one subset should be immutable, so they + Folders that have at least one product should be immutable, so they should not change path -> change of name or name of any parent is not possible. Args: project_name (str): Name of project. - folder_ids (Union[Iterable[str], None]): Limit folder ids filtering + folder_ids (Optional[Iterable[str]]): Limit folder ids filtering to a set of folders. If set to None all folders on project are checked. Returns: - set[str]: Folder ids that have at least one subset. + set[str]: Folder ids that have at least one product. """ if folder_ids is not None: @@ -2575,7 +2598,7 @@ class ServerAPI(object): query = folders_graphql_query({"id"}) query.set_variable_value("projectName", project_name) - query.set_variable_value("folderHasSubsets", True) + query.set_variable_value("folderHasProducts", True) if folder_ids: query.set_variable_value("folderIds", list(folder_ids)) @@ -2603,16 +2626,16 @@ class ServerAPI(object): project_name (str): Name of project. task_ids (Iterable[str]): Task ids to filter. task_names (Iterable[str]): Task names used for filtering. - task_types (Itartable[str]): Task types used for filtering. + task_types (Iterable[str]): Task types used for filtering. folder_ids (Iterable[str]): Ids of task parents. Use 'None' if folder is direct child of project. - active (Union[bool, None]): Filter active/inactive tasks. + active (Optional[bool]): Filter active/inactive tasks. Both are returned if is set to None. - fields (Union[Iterable[str], None]): Fields to be queried for + fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Generator[dict[str, Any]]: Queried task entities. @@ -2696,10 +2719,10 @@ class ServerAPI(object): entities. folder_id (str): Folder id. task_name (str): Task name - fields (Union[Iterable[str], None]): Fields that should be returned. + fields (Optional[Iterable[str]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Task entity data or None if was not found. @@ -2729,10 +2752,10 @@ class ServerAPI(object): project_name (str): Name of project where to look for queried entities. task_id (str): Task id. - fields (Union[Iterable[str], None]): Fields that should be returned. + fields (Optional[Iterable[str]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Task entity data or None if was not found. @@ -2748,32 +2771,32 @@ class ServerAPI(object): return task return None - def _filter_subset( - self, project_name, subset, active, own_attributes, use_rest + def _filter_product( + self, project_name, product, active, own_attributes, use_rest ): - if active is not None and subset["active"] is not active: + if active is not None and product["active"] is not active: return None if use_rest: - subset = self.get_rest_subset(project_name, subset["id"]) + product = self.get_rest_product(project_name, product["id"]) if own_attributes: - fill_own_attribs(subset) + fill_own_attribs(product) - return subset + return product - def get_subsets( + def get_products( self, project_name, - subset_ids=None, - subset_names=None, + product_ids=None, + product_names=None, folder_ids=None, names_by_folder_ids=None, active=True, fields=None, own_attributes=False ): - """Query subsets from server. + """Query products from server. Todos: Separate 'name_by_folder_ids' filtering to separated method. It @@ -2781,36 +2804,37 @@ class ServerAPI(object): Args: project_name (str): Name of project. - subset_ids (Iterable[str]): Task ids to filter. - subset_names (Iterable[str]): Task names used for filtering. - folder_ids (Iterable[str]): Ids of task parents. Use 'None' - if folder is direct child of project. - names_by_folder_ids (dict[str, Iterable[str]]): Subset name - filtering by folder id. - active (Union[bool, None]): Filter active/inactive subsets. + product_ids (Optional[Iterable[str]]): Task ids to filter. + product_names (Optional[Iterable[str]]): Task names used for + filtering. + folder_ids (Optional[Iterable[str]]): Ids of task parents. + Use 'None' if folder is direct child of project. + names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product + name filtering by folder id. + active (Optional[bool]): Filter active/inactive products. Both are returned if is set to None. - fields (Union[Iterable[str], None]): Fields to be queried for + fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Generator[dict[str, Any]]: Queried subset entities. + Generator[dict[str, Any]]: Queried product entities. """ if not project_name: return - if subset_ids is not None: - subset_ids = set(subset_ids) - if not subset_ids: + if product_ids is not None: + product_ids = set(product_ids) + if not product_ids: return - filter_subset_names = None - if subset_names is not None: - filter_subset_names = set(subset_names) - if not filter_subset_names: + filter_product_names = None + if product_names is not None: + filter_product_names = set(product_names) + if not filter_product_names: return filter_folder_ids = None @@ -2819,25 +2843,25 @@ class ServerAPI(object): if not filter_folder_ids: return - # This will disable 'folder_ids' and 'subset_names' filters + # This will disable 'folder_ids' and 'product_names' filters # - maybe could be enhanced in future? if names_by_folder_ids is not None: - filter_subset_names = set() + filter_product_names = set() filter_folder_ids = set() for folder_id, names in names_by_folder_ids.items(): if folder_id and names: filter_folder_ids.add(folder_id) - filter_subset_names |= set(names) + filter_product_names |= set(names) - if not filter_subset_names or not filter_folder_ids: + if not filter_product_names or not filter_folder_ids: return # Convert fields and add minimum required fields if fields: fields = set(fields) | {"id"} else: - fields = self.get_default_fields_for_type("subset") + fields = self.get_default_fields_for_type("product") use_rest = False if "data" in fields: @@ -2862,155 +2886,200 @@ class ServerAPI(object): if filter_folder_ids: filters["folderIds"] = list(filter_folder_ids) - if subset_ids: - filters["subsetIds"] = list(subset_ids) + if product_ids: + filters["productIds"] = list(product_ids) - if filter_subset_names: - filters["subsetNames"] = list(filter_subset_names) + if filter_product_names: + filters["productNames"] = list(filter_product_names) - query = subsets_graphql_query(fields) + query = products_graphql_query(fields) for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) parsed_data = query.query(self) - subsets = parsed_data.get("project", {}).get("subsets", []) - # Filter subsets by 'names_by_folder_ids' + products = parsed_data.get("project", {}).get("products", []) + # Filter products by 'names_by_folder_ids' if names_by_folder_ids: - subsets_by_folder_id = collections.defaultdict(list) - for subset in subsets: - filtered_subset = self._filter_subset( - project_name, subset, active, own_attributes, use_rest + products_by_folder_id = collections.defaultdict(list) + for product in products: + filtered_product = self._filter_product( + project_name, product, active, own_attributes, use_rest ) - if filtered_subset is not None: - folder_id = filtered_subset["folderId"] - subsets_by_folder_id[folder_id].append(filtered_subset) + if filtered_product is not None: + folder_id = filtered_product["folderId"] + products_by_folder_id[folder_id].append(filtered_product) for folder_id, names in names_by_folder_ids.items(): - for folder_subset in subsets_by_folder_id[folder_id]: - if folder_subset["name"] in names: - yield folder_subset + for folder_product in products_by_folder_id[folder_id]: + if folder_product["name"] in names: + yield folder_product else: - for subset in subsets: - filtered_subset = self._filter_subset( - project_name, subset, active, own_attributes, use_rest + for product in products: + filtered_product = self._filter_product( + project_name, product, active, own_attributes, use_rest ) - if filtered_subset is not None: - yield filtered_subset + if filtered_product is not None: + yield filtered_product - def get_subset_by_id( + def get_product_by_id( self, project_name, - subset_id, + product_id, fields=None, own_attributes=False ): - """Query subset entity by id. + """Query product entity by id. Args: project_name (str): Name of project where to look for queried entities. - subset_id (str): Subset id. - fields (Union[Iterable[str], None]): Fields that should be returned. + product_id (str): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Union[dict, None]: Subset entity data or None if was not found. + Union[dict, None]: Product entity data or None if was not found. """ - subsets = self.get_subsets( + products = self.get_products( project_name, - subset_ids=[subset_id], + product_ids=[product_id], active=None, fields=fields, own_attributes=own_attributes ) - for subset in subsets: - return subset + for product in products: + return product return None - def get_subset_by_name( + def get_product_by_name( self, project_name, - subset_name, + product_name, folder_id, fields=None, own_attributes=False ): - """Query subset entity by name and folder id. + """Query product entity by name and folder id. Args: project_name (str): Name of project where to look for queried entities. - subset_name (str): Subset name. - folder_id (str): Folder id (Folder is a parent of subsets). - fields (Union[Iterable[str], None]): Fields that should be returned. + product_name (str): Product name. + folder_id (str): Folder id (Folder is a parent of products). + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Union[dict, None]: Subset entity data or None if was not found. + Union[dict, None]: Product entity data or None if was not found. """ - subsets = self.get_subsets( + products = self.get_products( project_name, - subset_names=[subset_name], + product_names=[product_name], folder_ids=[folder_id], active=None, fields=fields, own_attributes=own_attributes ) - for subset in subsets: - return subset + for product in products: + return product return None - def get_subset_families(self, project_name, subset_ids=None): - """Families of subsets from a project. + def get_product_types(self, fields=None): + """Types of products. + + This is server wide information. Product types have 'name', 'icon' and + 'color'. Args: - project_name (str): Name of project where to look for queried - entities. - subset_ids (Union[None, Iterable[str]]): Limit filtering to set - of subset ids. + fields (Optional[Iterable[str]]): Product types fields to query. Returns: - set[str]: Families found on subsets. + list[dict[str, Any]]: Product types information. """ - if subset_ids is not None: - subsets = self.get_subsets( - project_name, - subset_ids=subset_ids, - fields=["family"], - active=None, - ) - return { - subset["family"] - for subset in subsets - } + if not fields: + fields = self.get_default_fields_for_type("productType") - query = GraphQlQuery("SubsetFamilies") - project_name_var = query.add_variable( - "projectName", "String!", project_name - ) - project_query = query.add_field("project") - project_query.set_filter("name", project_name_var) - project_query.add_field("subsetFamilies") + query = product_types_query(fields) parsed_data = query.query(self) - return set(parsed_data.get("project", {}).get("subsetFamilies", [])) + return parsed_data.get("productTypes", []) + + def get_project_product_types(self, project_name, fields=None): + """Types of products available on a project. + + Filter only product types available on project. + + Args: + project_name (str): Name of project where to look for + product types. + fields (Optional[Iterable[str]]): Product types fields to query. + + Returns: + list[dict[str, Any]]: Product types information. + """ + + if not fields: + fields = self.get_default_fields_for_type("productType") + + query = project_product_types_query(fields) + query.set_variable_value("projectName", project_name) + + parsed_data = query.query(self) + + return parsed_data.get("project", {}).get("productTypes", []) + + def get_product_type_names(self, project_name=None, product_ids=None): + """Product type names. + + Warnings: + This function will be probably removed. Matters if 'products_id' + filter has real use-case. + + Args: + project_name (Optional[str]): Name of project where to look for + queried entities. + product_ids (Optional[Iterable[str]]): Product ids filter. Can be + used only with 'project_name'. + + Returns: + set[str]: Product type names. + """ + + if project_name and product_ids: + products = self.get_products( + project_name, + product_ids=product_ids, + fields=["productType"], + active=None, + ) + return { + product["productType"] + for product in products + } + + return { + product_info["name"] + for product_info in self.get_project_product_types( + project_name, fields=["name"] + ) + } def get_versions( self, project_name, version_ids=None, - subset_ids=None, + product_ids=None, versions=None, hero=True, standard=True, @@ -3023,23 +3092,24 @@ class ServerAPI(object): Args: project_name (str): Name of project where to look for versions. - version_ids (Iterable[str]): Version ids used for version - filtering. - subset_ids (Iterable[str]): Subset ids used for version filtering. - versions (Iterable[int]): Versions we're interested in. - hero (bool): Receive also hero versions when set to true. - standard (bool): Receive versions which are not hero when + version_ids (Optional[Iterable[str]]): Version ids used for + version filtering. + product_ids (Optional[Iterable[str]]): Product ids used for + version filtering. + versions (Optional[Iterable[int]]): Versions we're interested in. + hero (Optional[bool]): Receive also hero versions when set to true. + standard (Optional[bool]): Receive versions which are not hero when set to true. - latest (bool): Return only latest version of standard versions. - This can be combined only with 'standard' attribute + latest (Optional[bool]): Return only latest version of standard + versions. This can be combined only with 'standard' attribute set to True. - active (Union[bool, None]): Receive active/inactive entities. + active (Optional[bool]): Receive active/inactive entities. Both are returned when 'None' is passed. - fields (Union[Iterable[str], None]): Fields to be queried + fields (Optional[Iterable[str]]): Fields to be queried for version. All possible folder fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Generator[Dict[str, Any]]: Queried version entities. @@ -3072,13 +3142,13 @@ class ServerAPI(object): return filters["versionIds"] = list(version_ids) - if subset_ids is not None: - subset_ids = set(subset_ids) - if not subset_ids: + if product_ids is not None: + product_ids = set(product_ids) + if not product_ids: return - filters["subsetIds"] = list(subset_ids) + filters["productIds"] = list(product_ids) - # TODO versions can't be used as fitler at this moment! + # TODO versions can't be used as filter at this moment! if versions is not None: versions = set(versions) if not versions: @@ -3152,10 +3222,10 @@ class ServerAPI(object): project_name (str): Name of project where to look for queried entities. version_id (str): Version id. - fields (Union[Iterable[str], None]): Fields that should be returned. + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Version entity data or None if was not found. @@ -3177,21 +3247,21 @@ class ServerAPI(object): self, project_name, version, - subset_id, + product_id, fields=None, own_attributes=False ): - """Query version entity by version and subset id. + """Query version entity by version and product id. Args: project_name (str): Name of project where to look for queried entities. version (int): Version of version entity. - subset_id (str): Subset id. Subset is a parent of version. - fields (Union[Iterable[str], None]): Fields that should be returned. + product_id (str): Product id. Product is a parent of version. + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Version entity data or None if was not found. @@ -3199,7 +3269,7 @@ class ServerAPI(object): versions = self.get_versions( project_name, - subset_ids=[subset_id], + product_ids=[product_id], versions=[version], active=None, fields=fields, @@ -3222,10 +3292,10 @@ class ServerAPI(object): project_name (str): Name of project where to look for queried entities. version_id (int): Hero version id. - fields (Union[Iterable[str], None]): Fields that should be returned. + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Version entity data or None if was not found. @@ -3241,25 +3311,25 @@ class ServerAPI(object): return version return None - def get_hero_version_by_subset_id( + def get_hero_version_by_product_id( self, project_name, - subset_id, + product_id, fields=None, own_attributes=False ): - """Query hero version entity by subset id. + """Query hero version entity by product id. - Only one hero version is available on a subset. + Only one hero version is available on a product. Args: project_name (str): Name of project where to look for queried entities. - subset_id (int): Subset id. - fields (Union[Iterable[str], None]): Fields that should be returned. + product_id (int): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Version entity data or None if was not found. @@ -3267,7 +3337,7 @@ class ServerAPI(object): versions = self.get_hero_versions( project_name, - subset_ids=[subset_id], + product_ids=[product_id], fields=fields, own_attributes=own_attributes ) @@ -3278,7 +3348,7 @@ class ServerAPI(object): def get_hero_versions( self, project_name, - subset_ids=None, + product_ids=None, version_ids=None, active=True, fields=None, @@ -3286,19 +3356,19 @@ class ServerAPI(object): ): """Query hero versions by multiple filters. - Only one hero version is available on a subset. + Only one hero version is available on a product. Args: project_name (str): Name of project where to look for queried entities. - subset_ids (int): Subset ids. - version_ids (int): Version ids. - active (Union[bool, None]): Receive active/inactive entities. + product_ids (Optional[Iterable[str]]): Product ids. + version_ids (Optional[Iterable[str]]): Version ids. + active (Optional[bool]): Receive active/inactive entities. Both are returned when 'None' is passed. - fields (Union[Iterable[str], None]): Fields that should be returned. + fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict, None]: Version entity data or None if was not found. @@ -3307,7 +3377,7 @@ class ServerAPI(object): return self.get_versions( project_name, version_ids=version_ids, - subset_ids=subset_ids, + product_ids=product_ids, hero=True, standard=False, active=active, @@ -3318,30 +3388,30 @@ class ServerAPI(object): def get_last_versions( self, project_name, - subset_ids, + product_ids, active=True, fields=None, own_attributes=False ): - """Query last version entities by subset ids. + """Query last version entities by product ids. Args: project_name (str): Project where to look for representation. - subset_ids (Iterable[str]): Subset ids. - active (Union[bool, None]): Receive active/inactive entities. + product_ids (Iterable[str]): Product ids. + active (Optional[bool]): Receive active/inactive entities. Both are returned when 'None' is passed. - fields (Union[Iterable[str], None]): fields to be queried + fields (Optional[Iterable[str]]): fields to be queried for representations. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - dict[str, dict[str, Any]]: Last versions by subset id. + dict[str, dict[str, Any]]: Last versions by product id. """ versions = self.get_versions( project_name, - subset_ids=subset_ids, + product_ids=product_ids, latest=True, active=active, fields=fields, @@ -3352,25 +3422,25 @@ class ServerAPI(object): for version in versions } - def get_last_version_by_subset_id( + def get_last_version_by_product_id( self, project_name, - subset_id, + product_id, active=True, fields=None, own_attributes=False ): - """Query last version entity by subset id. + """Query last version entity by product id. Args: project_name (str): Project where to look for representation. - subset_id (str): Subset id. - active (Union[bool, None]): Receive active/inactive entities. + product_id (str): Product id. + active (Optional[bool]): Receive active/inactive entities. Both are returned when 'None' is passed. - fields (Union[Iterable[str], None]): fields to be queried + fields (Optional[Iterable[str]]): fields to be queried for representations. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict[str, Any], None]: Queried version entity or None. @@ -3378,7 +3448,7 @@ class ServerAPI(object): versions = self.get_versions( project_name, - subset_ids=[subset_id], + product_ids=[product_id], latest=True, active=active, fields=fields, @@ -3388,27 +3458,27 @@ class ServerAPI(object): return version return None - def get_last_version_by_subset_name( + def get_last_version_by_product_name( self, project_name, - subset_name, + product_name, folder_id, active=True, fields=None, own_attributes=False ): - """Query last version entity by subset name and folder id. + """Query last version entity by product name and folder id. Args: project_name (str): Project where to look for representation. - subset_name (str): Subset name. + product_name (str): Product name. folder_id (str): Folder id. - active (Union[bool, None]): Receive active/inactive entities. + active (Optional[bool]): Receive active/inactive entities. Both are returned when 'None' is passed. - fields (Union[Iterable[str], None]): fields to be queried + fields (Optional[Iterable[str]): fields to be queried for representations. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict[str, Any], None]: Queried version entity or None. @@ -3417,21 +3487,21 @@ class ServerAPI(object): if not folder_id: return None - subset = self.get_subset_by_name( - project_name, subset_name, folder_id, fields=["_id"] + product = self.get_product_by_name( + project_name, product_name, folder_id, fields=["_id"] ) - if not subset: + if not product: return None - return self.get_last_version_by_subset_id( + return self.get_last_version_by_product_id( project_name, - subset["id"], + product["id"], active=active, fields=fields, own_attributes=own_attributes ) def version_is_latest(self, project_name, version_id): - """Is version latest from a subset. + """Is version latest from a product. Args: project_name (str): Project where to look for representation. @@ -3452,13 +3522,13 @@ class ServerAPI(object): project_query.set_filter("name", project_name_var) version_query = project_query.add_field("version") version_query.set_filter("id", version_id_var) - subset_query = version_query.add_field("subset") - latest_version_query = subset_query.add_field("latestVersion") + product_query = version_query.add_field("product") + latest_version_query = product_query.add_field("latestVersion") latest_version_query.add_field("id") parsed_data = query.query(self) latest_version = ( - parsed_data["project"]["version"]["subset"]["latestVersion"] + parsed_data["project"]["version"]["product"]["latestVersion"] ) return latest_version["id"] == version_id @@ -3481,22 +3551,23 @@ class ServerAPI(object): Args: project_name (str): Name of project where to look for versions. - representation_ids (Iterable[str]): Representation ids used for - representation filtering. - representation_names (Iterable[str]): Representation names used for - representation filtering. - version_ids (Iterable[str]): Version ids used for + representation_ids (Optional[Iterable[str]]): Representation ids + used for representation filtering. + representation_names (Optional[Iterable[str]]): Representation + names used for representation filtering. + version_ids (Optional[Iterable[str]]): Version ids used for representation filtering. Versions are parents of representations. - names_by_version_ids (bool): Find representations by names and - version ids. This filter discard all other filters. - active (Union[bool, None]): Receive active/inactive entities. + names_by_version_ids (Optional[bool]): Find representations + by names and version ids. This filter discard all + other filters. + active (Optional[bool]): Receive active/inactive entities. Both are returned when 'None' is passed. - fields (Union[Iterable[str], None]): Fields to be queried for + fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Generator[Dict[str, Any]]: Queried representation entities. @@ -3594,10 +3665,10 @@ class ServerAPI(object): Args: project_name (str): Project where to look for representation. representation_id (str): Id of representation. - fields (Union[Iterable[str], None]): fields to be queried + fields (Optional[Iterable[str]]): fields to be queried for representations. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict[str, Any], None]: Queried representation entity or None. @@ -3628,10 +3699,10 @@ class ServerAPI(object): project_name (str): Project where to look for representation. representation_name (str): Representation name. version_id (str): Version id. - fields (Union[Iterable[str], None]): fields to be queried + fields (Optional[Iterable[str]]): fields to be queried for representations. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict[str, Any], None]: Queried representation entity or None. @@ -3674,11 +3745,11 @@ class ServerAPI(object): } version_fields = self.get_default_fields_for_type("version") - subset_fields = self.get_default_fields_for_type("subset") + product_fields = self.get_default_fields_for_type("product") folder_fields = self.get_default_fields_for_type("folder") query = representations_parents_qraphql_query( - version_fields, subset_fields, folder_fields + version_fields, product_fields, folder_fields ) query.set_variable_value("projectName", project_name) query.set_variable_value("representationIds", list(repre_ids)) @@ -3687,10 +3758,10 @@ class ServerAPI(object): for repre in parsed_data["project"]["representations"]: repre_id = repre["id"] version = repre.pop("version") - subset = version.pop("subset") - folder = subset.pop("folder") + product = version.pop("product") + folder = product.pop("folder") output[repre_id] = RepresentationParents( - version, subset, folder, project + version, product, folder, project ) return output @@ -3726,9 +3797,10 @@ class ServerAPI(object): """Find representation ids which match passed context filters. Each representation has context integrated on representation entity in - database. The context may contain project, folder, task name or subset, - family and many more. This implementation gives option to quickly - filter representation based on representation data in database. + database. The context may contain project, folder, task name or + product name, product type and many more. This implementation gives + option to quickly filter representation based on representation data + in database. Context filters have defined structure. To define filter of nested subfield use dot '.' as delimiter (For example 'task.name'). @@ -3737,10 +3809,11 @@ class ServerAPI(object): Args: project_name (str): Project where to look for representations. context_filters (dict[str, list[str]]): Filters of context fields. - representation_names (Iterable[str]): Representation names, can be - used as additional filter for representations by their names. - version_ids (Iterable[str]): Version ids, can be used as additional - filter for representations by their parent ids. + representation_names (Optional[Iterable[str]]): Representation + names, can be used as additional filter for representations + by their names. + version_ids (Optional[Iterable[str]]): Version ids, can be used + as additional filter for representations by their parent ids. Returns: list[str]: Representation ids that match passed filters. @@ -3752,7 +3825,7 @@ class ServerAPI(object): >>> project_name = "testProject" >>> filters = { ... "task.name": ["[aA]nimation"], - ... "subset": [".*[Mm]ain"] + ... "product": [".*[Mm]ain"] ... } >>> repre_ids = get_repre_ids_by_context_filters( ... project_name, filters) @@ -3818,11 +3891,11 @@ class ServerAPI(object): workfile_ids (Optional[Iterable[str]]): Workfile ids. task_ids (Optional[Iterable[str]]): Task ids. paths (Optional[Iterable[str]]): Rootless workfiles paths. - fields (Union[Iterable[str], None]): Fields to be queried for + fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Generator[dict[str, Any]]: Queried workfile info entites. @@ -3873,11 +3946,11 @@ class ServerAPI(object): project_name (str): Project under which the entity is located. task_id (str): Task id. path (str): Rootless workfile path. - fields (Union[Iterable[str], None]): Fields to be queried for + fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict[str, Any], None]: Workfile info entity or None. @@ -3904,11 +3977,11 @@ class ServerAPI(object): Args: project_name (str): Project under which the entity is located. workfile_id (str): Workfile info id. - fields (Union[Iterable[str], None]): Fields to be queried for + fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (bool): Attribute values that are not explicitly set - on entity will have 'None' value. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: Union[dict[str, Any], None]: Workfile info entity or None. @@ -3944,18 +4017,18 @@ class ServerAPI(object): 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. + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. Args: 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 (str): Prepared thumbnail id from entity. Used only - to check if thumbnail was already cached. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - Union[str, None]: Path to downlaoded thumbnail or none if entity + Union[str, None]: Path to downloaded thumbnail or none if entity does not have any (or if user does not have permissions). """ @@ -3989,7 +4062,7 @@ class ServerAPI(object): if thumbnail_id is None: return None - # Cache thumbnail and return it's path + # Cache thumbnail and return path return self._thumbnail_cache.store_thumbnail( project_name, thumbnail_id, @@ -4005,11 +4078,11 @@ class ServerAPI(object): Args: project_name (str): Project under which the entity is located. folder_id (str): Folder id for which thumbnail should be returned. - thumbnail_id (str): Prepared thumbnail id from entity. Used only - to check if thumbnail was already cached. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - Union[str, None]: Path to downlaoded thumbnail or none if entity + Union[str, None]: Path to downloaded thumbnail or none if entity does not have any (or if user does not have permissions). """ @@ -4026,11 +4099,11 @@ class ServerAPI(object): project_name (str): Project under which the entity is located. version_id (str): Version id for which thumbnail should be returned. - thumbnail_id (str): Prepared thumbnail id from entity. Used only - to check if thumbnail was already cached. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - Union[str, None]: Path to downlaoded thumbnail or none if entity + Union[str, None]: Path to downloaded thumbnail or none if entity does not have any (or if user does not have permissions). """ @@ -4047,11 +4120,11 @@ class ServerAPI(object): project_name (str): Project under which the entity is located. workfile_id (str): Worfile id for which thumbnail should be returned. - thumbnail_id (str): Prepared thumbnail id from entity. Used only - to check if thumbnail was already cached. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - Union[str, None]: Path to downlaoded thumbnail or none if entity + Union[str, None]: Path to downloaded thumbnail or none if entity does not have any (or if user does not have permissions). """ @@ -4089,7 +4162,7 @@ class ServerAPI(object): project_name (str): Project where the thumbnail will be created and can be used. src_filepath (str): Filepath to thumbnail which should be uploaded. - thumbnail_id (str): Prepared if of thumbnail. + thumbnail_id (Optional[str]): Prepared if of thumbnail. Returns: str: Created thumbnail id. @@ -4161,7 +4234,7 @@ class ServerAPI(object): This project creation function is not validating project entity on creation. It is because project entity is created blindly with only - minimum required information about project which is it's name, code. + minimum required information about project which is name and code. Entered project name must be unique and project must not exist yet. @@ -4172,9 +4245,9 @@ class ServerAPI(object): Args: project_name (str): New project name. Should be unique. project_code (str): Project's code should be unique too. - library_project (bool): Project is library project. - preset_name (str): Name of anatomy preset. Default is used if not - passed. + library_project (Optional[bool]): Project is library project. + preset_name (Optional[str]): Name of anatomy preset. Default is + used if not passed. Raises: ValueError: When project name already exists. @@ -4498,12 +4571,12 @@ class ServerAPI(object): Args: project_name (str): Project where links are. - entity_type (Literal["folder", "task", "subset", + entity_type (Literal["folder", "task", "product", "version", "representations"]): Entity type. - entity_ids (Union[Iterable[str], None]): Ids of entities for which + entity_ids (Optional[Iterable[str]]): Ids of entities for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: @@ -4518,10 +4591,10 @@ class ServerAPI(object): query_func = tasks_graphql_query id_filter_key = "taskIds" project_sub_key = "tasks" - elif entity_type == "subset": - query_func = subsets_graphql_query - id_filter_key = "subsetIds" - project_sub_key = "subsets" + elif entity_type == "product": + query_func = products_graphql_query + id_filter_key = "productIds" + project_sub_key = "products" elif entity_type == "version": query_func = versions_graphql_query id_filter_key = "versionIds" @@ -4534,7 +4607,7 @@ class ServerAPI(object): raise ValueError("Unknown type \"{}\". Expected {}".format( entity_type, ", ".join( - ("folder", "task", "subset", "version", "representation") + ("folder", "task", "product", "version", "representation") ) )) @@ -4572,10 +4645,10 @@ class ServerAPI(object): Args: project_name (str): Project where links are. - folder_ids (Union[Iterable[str], None]): Ids of folders for which + folder_ids (Optional[Iterable[str]]): Ids of folders for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: @@ -4597,9 +4670,9 @@ class ServerAPI(object): Args: project_name (str): Project where links are. - folder_id (str): Id of folder for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + folder_id (str): Folder id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: @@ -4621,10 +4694,10 @@ class ServerAPI(object): Args: project_name (str): Project where links are. - task_ids (Union[Iterable[str], None]): Ids of tasks for which + task_ids (Optional[Iterable[str]]): Ids of tasks for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: @@ -4646,9 +4719,9 @@ class ServerAPI(object): Args: project_name (str): Project where links are. - task_id (str): Id of task for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + task_id (str): Task id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: @@ -4659,54 +4732,54 @@ class ServerAPI(object): project_name, [task_id], link_types, link_direction )[task_id] - def get_subsets_links( + def get_products_links( self, project_name, - subset_ids=None, + product_ids=None, link_types=None, link_direction=None ): - """Query subsets links from server. + """Query products links from server. Args: project_name (str): Project where links are. - subset_ids (Union[Iterable[str], None]): Ids of subsets for which + product_ids (Optional[Iterable[str]]): Ids of products for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: - dict[str, list[dict[str, Any]]]: Link info by subset ids. + dict[str, list[dict[str, Any]]]: Link info by product ids. """ return self.get_entities_links( - project_name, "subset", subset_ids, link_types, link_direction + project_name, "product", product_ids, link_types, link_direction ) - def get_subset_links( + def get_product_links( self, project_name, - subset_id, + product_id, link_types=None, link_direction=None ): - """Query subset links from server. + """Query product links from server. Args: project_name (str): Project where links are. - subset_id (str): Id of subset for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + product_id (str): Product id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: - list[dict[str, Any]]: Link info of subset. + list[dict[str, Any]]: Link info of product. """ - return self.get_subsets_links( - project_name, [subset_id], link_types, link_direction - )[subset_id] + return self.get_products_links( + project_name, [product_id], link_types, link_direction + )[product_id] def get_versions_links( self, @@ -4719,10 +4792,10 @@ class ServerAPI(object): Args: project_name (str): Project where links are. - version_ids (Union[Iterable[str], None]): Ids of versions for which + version_ids (Optional[Iterable[str]]): Ids of versions for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: @@ -4744,9 +4817,9 @@ class ServerAPI(object): Args: project_name (str): Project where links are. - version_id (str): Id of version for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + version_id (str): Version id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: @@ -4768,10 +4841,10 @@ class ServerAPI(object): Args: project_name (str): Project where links are. - representation_ids (Union[Iterable[str], None]): Ids of + representation_ids (Optional[Iterable[str]]): Ids of representations for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: @@ -4797,10 +4870,10 @@ class ServerAPI(object): Args: project_name (str): Project where links are. - representation_id (str): Id of representation for which links + representation_id (str): Representation id for which links should be received. - link_types (Union[Iterable[str], None]): Link type filters. - link_direction (Union[Literal["in", "out"], None]): Link direction + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: @@ -4829,11 +4902,11 @@ class ServerAPI(object): project_name (str): On which project should be operations processed. operations (list[dict[str, Any]]): Operations to be processed. - can_fail (bool): Server will try to process all operations even if - one of them fails. - raise_on_fail (bool): Raise exception if an operation fails. - You can handle failed operations on your own when set to - 'False'. + can_fail (Optional[bool]): Server will try to process all + operations even if one of them fails. + raise_on_fail (Optional[bool]): Raise exception if an operation + fails. You can handle failed operations on your own + when set to 'False'. Raises: ValueError: Operations can't be converted to json string. diff --git a/openpype/vendor/python/common/ayon_api/utils.py b/openpype/vendor/python/common/ayon_api/utils.py index 28971f7de5..d1f108a220 100644 --- a/openpype/vendor/python/common/ayon_api/utils.py +++ b/openpype/vendor/python/common/ayon_api/utils.py @@ -22,7 +22,7 @@ SLUGIFY_SEP_WHITELIST = " ,./\\;:!|*^#@~+-_=" RepresentationParents = collections.namedtuple( "RepresentationParents", - ("version", "subset", "folder", "project") + ("version", "product", "folder", "project") ) diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index 9b38175335..d464469cf7 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.1.18" +__version__ = "0.2.0" From 553bba87131d83fc5b1ff164b12ef36a5e8ebf02 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Jun 2023 12:35:19 +0200 Subject: [PATCH 236/446] AYON: Fix site sync settings (#5069) * fix sync server settings * fix formatting --- openpype/settings/ayon_settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 46ad77c7ca..18df115dcf 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -278,7 +278,11 @@ def _convert_modules_system( if "enabled" not in value or module_name not in output_modules: continue - output_modules[module_name]["enabled"] = module_name in addon_versions + ayon_module_name = module_name + if module_name == "sync_server": + ayon_module_name = "sitesync" + output_modules[module_name]["enabled"] = ( + ayon_module_name in addon_versions) # Missing modules conversions # - "sync_server" -> renamed to sitesync From 9e5720953f95685a4feb8a731e3bfd53f5f643f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 1 Jun 2023 12:52:58 +0200 Subject: [PATCH 237/446] fix loader filepath access --- openpype/hosts/blender/plugins/load/load_camera_abc.py | 4 +++- openpype/hosts/fusion/plugins/load/load_workfile.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_camera_abc.py b/openpype/hosts/blender/plugins/load/load_camera_abc.py index 21b48f409f..e5afecff66 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_abc.py +++ b/openpype/hosts/blender/plugins/load/load_camera_abc.py @@ -81,7 +81,9 @@ class AbcCameraLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + + libpath = self.filepath_from_context(context) + asset = context["asset"]["name"] subset = context["subset"]["name"] diff --git a/openpype/hosts/fusion/plugins/load/load_workfile.py b/openpype/hosts/fusion/plugins/load/load_workfile.py index b49d104a15..14e36ca8fd 100644 --- a/openpype/hosts/fusion/plugins/load/load_workfile.py +++ b/openpype/hosts/fusion/plugins/load/load_workfile.py @@ -27,6 +27,7 @@ class FusionLoadWorkfile(load.LoaderPlugin): # Get needed elements bmd = get_bmd_library() comp = get_current_comp() + path = self.filepath_from_context(context) # Paste the content of the file into the current comp - comp.Paste(bmd.readfile(self.fname)) + comp.Paste(bmd.readfile(path)) From 13a6c94c86d6067b8846ef2def5bf358d2d22948 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 1 Jun 2023 13:04:11 +0200 Subject: [PATCH 238/446] change pyblish version in pyproject --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 63e9f4cd13..d319215ecd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ keyring = "^22.0.1" log4mongo = "^1.7" pathlib2= "^2.3.5" # deadline submit publish job only (single place, maybe not needed?) Pillow = "^9.0" # used in TVPaint and for slates -pyblish-base = "^1.8.8" +pyblish-base = "^1.8.11" pynput = "^1.7.2" # idle manager in tray pymongo = "^3.11.2" "Qt.py" = "^1.3.3" From 3869a6f1d08f164bd8dbad71846a07da65b9fab2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 Jun 2023 10:57:55 +0200 Subject: [PATCH 239/446] Igniter: QApplication is created (#5081) * get_qt_app actually creates new app * Single line creation --- igniter/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/igniter/__init__.py b/igniter/__init__.py index 04026d5a37..16ffb940f6 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -39,7 +39,8 @@ def _get_qt_app(): QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough ) - return app + return QtWidgets.QApplication(sys.argv) + def open_dialog(): """Show Igniter dialog.""" From 689d29abffc2ec37766e457d4ad5c714b23225ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 Jun 2023 10:58:09 +0200 Subject: [PATCH 240/446] use 'tlsCAFile' instead of deprecated 'ssl_ca_certs' (#5080) --- igniter/tools.py | 4 ++-- openpype/client/mongo/mongo.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/igniter/tools.py b/igniter/tools.py index df583fbb51..9dea203f0c 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -73,7 +73,7 @@ def validate_mongo_connection(cnx: str) -> (bool, str): } # Add certificate path if should be required if should_add_certificate_path_to_mongo_url(cnx): - kwargs["ssl_ca_certs"] = certifi.where() + kwargs["tlsCAFile"] = certifi.where() try: client = MongoClient(cnx, **kwargs) @@ -147,7 +147,7 @@ def get_openpype_global_settings(url: str) -> dict: """ kwargs = {} if should_add_certificate_path_to_mongo_url(url): - kwargs["ssl_ca_certs"] = certifi.where() + kwargs["tlsCAFile"] = certifi.where() try: # Create mongo connection diff --git a/openpype/client/mongo/mongo.py b/openpype/client/mongo/mongo.py index ce8d35fcdd..2be426efeb 100644 --- a/openpype/client/mongo/mongo.py +++ b/openpype/client/mongo/mongo.py @@ -224,7 +224,7 @@ class OpenPypeMongoConnection: "serverSelectionTimeoutMS": timeout } if should_add_certificate_path_to_mongo_url(mongo_url): - kwargs["ssl_ca_certs"] = certifi.where() + kwargs["tlsCAFile"] = certifi.where() mongo_client = pymongo.MongoClient(mongo_url, **kwargs) From 0fab8694b6adcff75b01509701d637fa5b30ac32 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 Jun 2023 18:22:41 +0200 Subject: [PATCH 241/446] use version 1.2.0 of unidecode module (#5090) --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index b5209404df..d62f47b2d4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3397,14 +3397,14 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "unidecode" -version = "1.3.6" +version = "1.2.0" description = "ASCII transliterations of Unicode text" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, - {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, + {file = "Unidecode-1.2.0-py2.py3-none-any.whl", hash = "sha256:12435ef2fc4cdfd9cf1035a1db7e98b6b047fe591892e81f34e94959591fad00"}, + {file = "Unidecode-1.2.0.tar.gz", hash = "sha256:8d73a97d387a956922344f6b74243c2c6771594659778744b2dbdaad8f6b727d"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index d319215ecd..03a7ceb1f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" -Unidecode = "^1.2" +Unidecode = "1.2.0" [tool.poetry.dev-dependencies] flake8 = "^6.0" From 255d0d6493d371a58f33648be4d54e4a245ff3bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 6 Jun 2023 13:42:37 +0200 Subject: [PATCH 242/446] lower cryptography to 39.0.0 --- poetry.lock | 54 ++++++++++++++++++++++++++------------------------ pyproject.toml | 1 + 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/poetry.lock b/poetry.lock index d62f47b2d4..d8bc441875 100644 --- a/poetry.lock +++ b/poetry.lock @@ -730,45 +730,47 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "40.0.2" +version = "39.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b"}, - {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440"}, - {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d"}, - {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288"}, - {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2"}, - {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b"}, - {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"}, - {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c"}, - {file = "cryptography-40.0.2-cp36-abi3-win32.whl", hash = "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9"}, - {file = "cryptography-40.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b"}, - {file = "cryptography-40.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b"}, - {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e"}, - {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a"}, - {file = "cryptography-40.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958"}, - {file = "cryptography-40.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b"}, - {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636"}, - {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e"}, - {file = "cryptography-40.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404"}, - {file = "cryptography-40.0.2.tar.gz", hash = "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99"}, + {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"}, + {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"}, + {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"}, + {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"}, + {file = "cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"}, + {file = "cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"}, + {file = "cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"}, ] [package.dependencies] cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "check-manifest", "mypy", "ruff"] +pep8test = ["black", "ruff"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] -tox = ["tox"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] name = "cx-freeze" @@ -3716,4 +3718,4 @@ docs = [] [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "cdf2ba74b9838635baddfd5d79ea94e10243db328fe6dc426455475b8a047671" +content-hash = "d2b8da22dcd11e0b03f19b9b79e51f205156c5ce75e41cc0225392e9afd8803b" diff --git a/pyproject.toml b/pyproject.toml index 03a7ceb1f3..1350b6e190 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" Unidecode = "1.2.0" +cryptography = "39.0.0" [tool.poetry.dev-dependencies] flake8 = "^6.0" From 2d0d0305bb1371a65f6f16953159b80c9ffb9768 Mon Sep 17 00:00:00 2001 From: sjt-rvx <72554834+sjt-rvx@users.noreply.github.com> Date: Thu, 8 Jun 2023 09:31:57 +0000 Subject: [PATCH 243/446] set the key to environment istead of environments (#5118) --- openpype/settings/ayon_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 18df115dcf..d7c8650b05 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -155,7 +155,7 @@ def _convert_general(ayon_settings, output, default_settings): "log_to_server": False, "studio_name": core_settings["studio_name"], "studio_code": core_settings["studio_code"], - "environments": environments + "environment": environments }) output["general"] = general From a08e59a5e35f9fc2dff61b471bbc1c25523e882c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 8 Jun 2023 11:33:48 +0200 Subject: [PATCH 244/446] General: CLI addon command (#5109) * added 'addon' as alias for 'module' * Changed docstring and description * formatting change --- openpype/cli.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index 17f340bee5..bc837cdeba 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -5,11 +5,24 @@ import sys import code import click -# import sys from .pype_commands import PypeCommands -@click.group(invoke_without_command=True) +class AliasedGroup(click.Group): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._aliases = {} + + def set_alias(self, src_name, dst_name): + self._aliases[dst_name] = src_name + + def get_command(self, ctx, cmd_name): + if cmd_name in self._aliases: + cmd_name = self._aliases[cmd_name] + return super().get_command(ctx, cmd_name) + + +@click.group(cls=AliasedGroup, invoke_without_command=True) @click.pass_context @click.option("--use-version", expose_value=False, help="use specified version") @@ -58,16 +71,20 @@ def tray(): @PypeCommands.add_modules -@main.group(help="Run command line arguments of OpenPype modules") +@main.group(help="Run command line arguments of OpenPype addons") @click.pass_context def module(ctx): - """Module specific commands created dynamically. + """Addon specific commands created dynamically. - These commands are generated dynamically by currently loaded addon/modules. + These commands are generated dynamically by currently loaded addons. """ pass +# Add 'addon' as alias for module +main.set_alias("module", "addon") + + @main.command() @click.option("--ftrack-url", envvar="FTRACK_SERVER", help="Ftrack server url") From b44ef670502f78e221dba1c4e4bf465524da7fb9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 8 Jun 2023 12:16:53 +0200 Subject: [PATCH 245/446] updated settings converison with latest addon settings --- openpype/settings/ayon_settings.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index d7c8650b05..d2a478e259 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -740,8 +740,9 @@ def _convert_nuke_project_settings(ayon_settings, output): "sync_workfile_version_on_product_types")) # TODO 'ExtractThumbnail' does not have ideal schema in v3 + ayon_extract_thumbnail = ayon_publish["ExtractThumbnail"] new_thumbnail_nodes = {} - for item in ayon_publish["ExtractThumbnail"]["nodes"]: + for item in ayon_extract_thumbnail["nodes"]: name = item["nodeclass"] value = [] for knob in _convert_nuke_knobs(item["knobs"]): @@ -754,7 +755,11 @@ def _convert_nuke_project_settings(ayon_settings, output): value.append([knob_name, knob_value]) new_thumbnail_nodes[name] = value - ayon_publish["ExtractThumbnail"]["nodes"] = new_thumbnail_nodes + ayon_extract_thumbnail["nodes"] = new_thumbnail_nodes + + if "reposition_nodes" in ayon_extract_thumbnail: + for item in ayon_extract_thumbnail["reposition_nodes"]: + item["knobs"] = _convert_nuke_knobs(item["knobs"]) # --- ImageIO --- # NOTE 'monitorOutLut' is maybe not yet in v3 (ut should be) @@ -857,14 +862,6 @@ def _convert_tvpaint_project_settings(ayon_settings, output): extract_sequence_setting["review_bg"] ) - # TODO remove when removed from OpenPype schema - # this is unused setting - ayon_tvpaint["publish"]["ExtractSequence"]["families_to_review"] = [ - "review", - "renderlayer", - "renderscene" - ] - output["tvpaint"] = ayon_tvpaint From 706469205cdf1a54b2a969ae2ff953dca1f70b23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 13 Jun 2023 11:34:45 +0200 Subject: [PATCH 246/446] updated ayon api to '0.2.1' --- .../vendor/python/common/ayon_api/__init__.py | 44 +- .../vendor/python/common/ayon_api/_api.py | 92 +++ .../python/common/ayon_api/entity_hub.py | 88 +++ .../python/common/ayon_api/server_api.py | 632 ++++++++++++++++-- .../vendor/python/common/ayon_api/version.py | 2 +- 5 files changed, 802 insertions(+), 56 deletions(-) diff --git a/openpype/vendor/python/common/ayon_api/__init__.py b/openpype/vendor/python/common/ayon_api/__init__.py index b1790d1fb6..6f791972cd 100644 --- a/openpype/vendor/python/common/ayon_api/__init__.py +++ b/openpype/vendor/python/common/ayon_api/__init__.py @@ -59,13 +59,31 @@ from ._api import ( get_addon_url, download_addon_private_file, + get_installers, + create_installer, + update_installer, + delete_installer, + download_installer, + upload_installer, + get_dependencies_info, update_dependency_info, + get_dependency_packages, + create_dependency_package, + update_dependency_package, + delete_dependency_package, download_dependency_package, upload_dependency_package, - delete_dependency_package, + get_bundles, + create_bundle, + update_bundle, + delete_bundle, + + get_info, + get_server_version, + get_server_version_tuple, get_user, get_users, @@ -87,9 +105,11 @@ from ._api import ( get_addons_project_settings, get_addons_settings, + get_project_names, get_projects, get_project, create_project, + update_project, delete_project, get_folder_by_id, @@ -218,13 +238,31 @@ __all__ = ( "get_addon_url", "download_addon_private_file", + "get_installers", + "create_installer", + "update_installer", + "delete_installer", + "download_installer", + "upload_installer", + "get_dependencies_info", "update_dependency_info", + "get_dependency_packages", + "create_dependency_package", + "update_dependency_package", + "delete_dependency_package", "download_dependency_package", "upload_dependency_package", - "delete_dependency_package", + "get_bundles", + "create_bundle", + "update_bundle", + "delete_bundle", + + "get_info", + "get_server_version", + "get_server_version_tuple", "get_user", "get_users", @@ -245,9 +283,11 @@ __all__ = ( "get_addons_project_settings", "get_addons_settings", + "get_project_names", "get_projects", "get_project", "create_project", + "update_project", "delete_project", "get_folder_by_id", diff --git a/openpype/vendor/python/common/ayon_api/_api.py b/openpype/vendor/python/common/ayon_api/_api.py index f351cd8102..f5a3a4787a 100644 --- a/openpype/vendor/python/common/ayon_api/_api.py +++ b/openpype/vendor/python/common/ayon_api/_api.py @@ -531,6 +531,53 @@ def download_addon_private_file(*args, **kwargs): return con.download_addon_private_file(*args, **kwargs) +def get_info(*args, **kwargs): + con = get_server_api_connection() + return con.get_info(*args, **kwargs) + + +def get_server_version(*args, **kwargs): + con = get_server_api_connection() + return con.get_server_version(*args, **kwargs) + + +def get_server_version_tuple(*args, **kwargs): + con = get_server_api_connection() + return con.get_server_version_tuple(*args, **kwargs) + + +# Installers +def get_installers(*args, **kwargs): + con = get_server_api_connection() + return con.get_installers(*args, **kwargs) + + +def create_installer(*args, **kwargs): + con = get_server_api_connection() + return con.create_installer(*args, **kwargs) + + +def update_installer(*args, **kwargs): + con = get_server_api_connection() + return con.update_installer(*args, **kwargs) + + +def delete_installer(*args, **kwargs): + con = get_server_api_connection() + return con.delete_installer(*args, **kwargs) + + +def download_installer(*args, **kwargs): + con = get_server_api_connection() + con.download_installer(*args, **kwargs) + + +def upload_installer(*args, **kwargs): + con = get_server_api_connection() + con.upload_installer(*args, **kwargs) + + +# Dependency packages def get_dependencies_info(*args, **kwargs): con = get_server_api_connection() return con.get_dependencies_info(*args, **kwargs) @@ -551,6 +598,21 @@ def upload_dependency_package(*args, **kwargs): return con.upload_dependency_package(*args, **kwargs) +def get_dependency_packages(*args, **kwargs): + con = get_server_api_connection() + return con.get_dependency_packages(*args, **kwargs) + + +def create_dependency_package(*args, **kwargs): + con = get_server_api_connection() + return con.create_dependency_package(*args, **kwargs) + + +def update_dependency_package(*args, **kwargs): + con = get_server_api_connection() + return con.update_dependency_package(*args, **kwargs) + + def delete_dependency_package(*args, **kwargs): con = get_server_api_connection() return con.delete_dependency_package(*args, **kwargs) @@ -561,6 +623,26 @@ def get_project_anatomy_presets(*args, **kwargs): return con.get_project_anatomy_presets(*args, **kwargs) +def get_bundles(*args, **kwargs): + con = get_server_api_connection() + return con.get_bundles(*args, **kwargs) + + +def create_bundle(*args, **kwargs): + con = get_server_api_connection() + return con.create_bundle(*args, **kwargs) + + +def update_bundle(*args, **kwargs): + con = get_server_api_connection() + return con.update_bundle(*args, **kwargs) + + +def delete_bundle(*args, **kwargs): + con = get_server_api_connection() + return con.delete_bundle(*args, **kwargs) + + def get_project_anatomy_preset(*args, **kwargs): con = get_server_api_connection() return con.get_project_anatomy_preset(*args, **kwargs) @@ -621,6 +703,11 @@ def get_addons_settings(*args, **kwargs): return con.get_addons_settings(*args, **kwargs) +def get_project_names(*args, **kwargs): + con = get_server_api_connection() + return con.get_project_names(*args, **kwargs) + + def get_project(*args, **kwargs): con = get_server_api_connection() return con.get_project(*args, **kwargs) @@ -801,6 +888,11 @@ def create_project( ) +def update_project(project_name, *args, **kwargs): + con = get_server_api_connection() + return con.update_project(project_name, *args, **kwargs) + + def delete_project(project_name): con = get_server_api_connection() return con.delete_project(project_name) diff --git a/openpype/vendor/python/common/ayon_api/entity_hub.py b/openpype/vendor/python/common/ayon_api/entity_hub.py index d71ce18839..ab1e2584d7 100644 --- a/openpype/vendor/python/common/ayon_api/entity_hub.py +++ b/openpype/vendor/python/common/ayon_api/entity_hub.py @@ -683,6 +683,82 @@ class EntityHub(object): "entityId": entity.id } + def _pre_commit_types_changes( + self, project_changes, orig_types, changes_key, post_changes + ): + """Compare changes of types on a project. + + Compare old and new types. Change project changes content if some old + types were removed. In that case the final change of types will + happen when all other entities have changed. + + Args: + project_changes (dict[str, Any]): Project changes. + orig_types (list[dict[str, Any]]): Original types. + changes_key (Literal[folderTypes, taskTypes]): Key of type changes + in project changes. + post_changes (dict[str, Any]): An object where post changes will + be stored. + """ + + if changes_key not in project_changes: + return + + new_types = project_changes[changes_key] + + orig_types_by_name = { + type_info["name"]: type_info + for type_info in orig_types + } + new_names = { + type_info["name"] + for type_info in new_types + } + diff_names = set(orig_types_by_name) - new_names + if not diff_names: + return + + # Create copy of folder type changes to post changes + # - post changes will be commited at the end + post_changes[changes_key] = copy.deepcopy(new_types) + + for type_name in diff_names: + new_types.append(orig_types_by_name[type_name]) + + def _pre_commit_project(self): + """Some project changes cannot be committed before hierarchy changes. + + It is not possible to change folder types or task types if there are + existing hierarchy items using the removed types. For that purposes + is first committed union of all old and new types and post changes + are prepared when all existing entities are changed. + + Returns: + dict[str, Any]: Changes that will be committed after hierarchy + changes. + """ + + project_changes = self.project_entity.changes + + post_changes = {} + if not project_changes: + return post_changes + + self._pre_commit_types_changes( + project_changes, + self.project_entity.get_orig_folder_types(), + "folderType", + post_changes + ) + self._pre_commit_types_changes( + project_changes, + self.project_entity.get_orig_task_types(), + "taskType", + post_changes + ) + self._connection.update_project(self.project_name, **project_changes) + return post_changes + def commit_changes(self): """Commit any changes that happened on entities. @@ -690,6 +766,9 @@ class EntityHub(object): Use Operations Session instead of known operations body. """ + post_project_changes = self._pre_commit_project() + self.project_entity.lock() + project_changes = self.project_entity.changes if project_changes: response = self._connection.patch( @@ -760,6 +839,9 @@ class EntityHub(object): self._connection.send_batch_operations( self.project_name, operations_body ) + if post_project_changes: + self._connection.update_project( + self.project_name, **post_project_changes) self.lock() @@ -1463,6 +1545,9 @@ class ProjectEntity(BaseEntity): parent = property(get_parent, set_parent) + def get_orig_folder_types(self): + return copy.deepcopy(self._orig_folder_types) + def get_folder_types(self): return copy.deepcopy(self._folder_types) @@ -1474,6 +1559,9 @@ class ProjectEntity(BaseEntity): new_folder_types.append(folder_type) self._folder_types = new_folder_types + def get_orig_task_types(self): + return copy.deepcopy(self._orig_task_types) + def get_task_types(self): return copy.deepcopy(self._task_types) diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index 253d590658..8cbb23ce0d 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -4,6 +4,7 @@ import io import json import logging import collections +import datetime import platform import copy import uuid @@ -70,6 +71,14 @@ PROJECT_NAME_REGEX = re.compile( "^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS) ) +VERSION_REGEX = re.compile( + r"(?P0|[1-9]\d*)" + r"\.(?P0|[1-9]\d*)" + r"\.(?P0|[1-9]\d*)" + r"(?:-(?P[a-zA-Z\d\-.]*))?" + r"(?:\+(?P[a-zA-Z\d\-.]*))?" +) + def _get_description(response): if HTTPStatus is None: @@ -329,6 +338,8 @@ class ServerAPI(object): self._access_token_is_service = None self._token_is_valid = None self._server_available = None + self._server_version = None + self._server_version_tuple = None self._session = None @@ -631,12 +642,52 @@ class ServerAPI(object): Use this method for validation of token instead of 'get_user'. Returns: - Dict[str, Any]: Information from server. + dict[str, Any]: Information from server. """ response = self.get("info") return response.data + def get_server_version(self): + """Get server version. + + Version should match semantic version (https://semver.org/). + + Returns: + str: Server version. + """ + + if self._server_version is None: + self._server_version = self.get_info()["version"] + return self._server_version + + def get_server_version_tuple(self): + """Get server version as tuple. + + Version should match semantic version (https://semver.org/). + + This function only returns first three numbers of version. + + Returns: + Tuple[int, int, int, Union[str, None], Union[str, None]]: Server + version. + """ + + if self._server_version_tuple is None: + re_match = VERSION_REGEX.fullmatch( + self.get_server_version()) + self._server_version_tuple = ( + int(re_match.group("major")), + int(re_match.group("minor")), + int(re_match.group("patch")), + re_match.group("prerelease"), + re_match.group("buildmetadata") + ) + return self._server_version_tuple + + server_version = property(get_server_version) + server_version_tuple = property(get_server_version_tuple) + def _get_user_info(self): if self._access_token is None: return None @@ -869,7 +920,7 @@ class ServerAPI(object): event_id (str): Id of event. Returns: - Dict[str, Any]: Full event data. + dict[str, Any]: Full event data. """ response = self.get("events/{}".format(event_id)) @@ -902,7 +953,7 @@ class ServerAPI(object): for each event. Returns: - Generator[Dict[str, Any]]: Available events matching filters. + Generator[dict[str, Any]]: Available events matching filters. """ filters = {} @@ -1269,7 +1320,7 @@ class ServerAPI(object): Cache schema - How to find out it is outdated? Returns: - Dict[str, Any]: Full server schema. + dict[str, Any]: Full server schema. """ url = "{}/openapi.json".format(self._base_url) @@ -1287,7 +1338,7 @@ class ServerAPI(object): e.g. 'config' has just object definition without reference schema. Returns: - Dict[str, Any]: Component schemas. + dict[str, Any]: Component schemas. """ server_schema = self.get_server_schema() @@ -1395,7 +1446,7 @@ class ServerAPI(object): received. Returns: - Dict[str, Dict[str, Any]]: Attribute schemas that are available + dict[str, dict[str, Any]]: Attribute schemas that are available for entered entity type. """ attributes = self._entity_type_attributes_cache.get(entity_type) @@ -1563,6 +1614,148 @@ class ServerAPI(object): ) return dst_filepath + def get_installers(self, version=None, platform_name=None): + """Information about desktop application installers on server. + + Desktop application installers are helpers to download/update AYON + desktop application for artists. + + Args: + version (Optional[str]): Filter installers by version. + platform_name (Optional[str]): Filter installers by platform name. + + Returns: + list[dict[str, Any]]: + """ + + query_fields = [ + "{}={}".format(key, value) + for key, value in ( + ("version", version), + ("platform", platform_name), + ) + if value + ] + query = "" + if query_fields: + query = "?{}".format(",".join(query_fields)) + + response = self.get("desktop/installers{}".format(query)) + response.raise_for_status() + return response.data + + def create_installer( + self, + filename, + version, + python_version, + platform_name, + python_modules, + checksum, + checksum_type, + file_size, + sources=None, + ): + """Create new installer information on server. + + This step will create only metadata. Make sure to upload installer + to the server using 'upload_installer' method. + + Args: + filename (str): Installer filename. + version (str): Version of installer. + python_version (str): Version of Python. + platform_name (str): Name of platform. + python_modules (dict[str, str]): Python modules that are available + in installer. + checksum (str): Installer file checksum. + checksum_type (str): Type of checksum used to create checksum. + file_size (int): File size. + sources (Optional[list[dict[str, Any]]]): List of sources that + can be used to download file. + """ + + body = { + "filename": filename, + "version": version, + "pythonVersion": python_version, + "platform": platform_name, + "pythonModules": python_modules, + "checksum": checksum, + "checksumType": checksum_type, + "size": file_size, + } + if sources: + body["sources"] = sources + + response = self.post("desktop/installers", **body) + response.raise_for_status() + + def update_installer(self, filename, sources): + """Update installer information on server. + + Args: + filename (str): Installer filename. + sources (list[dict[str, Any]]): List of sources that + can be used to download file. Fully replaces existing sources. + """ + + response = self.post( + "desktop/installers/{}".format(filename), + sources=sources + ) + response.raise_for_status() + + def delete_installer(self, filename): + """Delete installer from server. + + Args: + filename (str): Installer filename. + """ + + response = self.delete("dekstop/installers/{}".format(filename)) + response.raise_for_status() + + def download_installer( + self, + filename, + dst_filepath, + chunk_size=None, + progress=None + ): + """Download installer file from server. + + Args: + filename (str): Installer filename. + dst_filepath (str): Destination filepath. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + """ + + self.download_file( + "desktop/installers/{}".format(filename), + dst_filepath, + chunk_size=chunk_size, + progress=progress + ) + + def upload_installer(self, src_filepath, dst_filename, progress=None): + """Upload installer file to server. + + Args: + src_filepath (str): Source filepath. + dst_filename (str): Destination filename. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + """ + + self.upload_file( + "desktop/installers/{}".format(dst_filename), + src_filepath, + progress=progress + ) + def get_dependencies_info(self): """Information about dependency packages on server. @@ -1581,13 +1774,22 @@ class ServerAPI(object): "productionPackage": str } + Deprecated: + Deprecated since server version 0.2.1. Use + 'get_dependency_packages' instead. + Returns: dict[str, Any]: Information about dependency packages known for server. """ - result = self.get("dependencies") - return result.data + major, minor, patch, _, _ = self.server_version_tuple + if major == 0 and (minor < 2 or (minor == 2 and patch < 1)): + result = self.get("dependencies") + return result.data + packages = self.get_dependency_packages() + packages["productionPackage"] = None + return packages def update_dependency_info( self, @@ -1604,21 +1806,26 @@ class ServerAPI(object): The endpoint can be used to create or update dependency package. + + Deprecated: + Deprecated for server version 0.2.1. Use + 'create_dependency_pacakge' instead. + Args: name (str): Name of dependency package. - platform_name (Literal["windows", "linux", "darwin"]): Platform for - which is dependency package targeted. + platform_name (Literal["windows", "linux", "darwin"]): Platform + for which is dependency package targeted. size (int): Size of dependency package in bytes. checksum (str): Checksum of archive file where dependencies are. checksum_algorithm (Optional[str]): Algorithm used to calculate checksum. By default, is used 'md5' (defined by server). - supported_addons (Optional[Dict[str, str]]): Name of addons for + supported_addons (Optional[dict[str, str]]): Name of addons for which was the package created. '{"": "", ...}' - python_modules (Optional[Dict[str, str]]): Python modules in + python_modules (Optional[dict[str, str]]): Python modules in dependencies package. '{"": "", ...}' - sources (Optional[List[Dict[str, Any]]]): Information about + sources (Optional[list[dict[str, Any]]]): Information about sources where dependency package is available. """ @@ -1645,27 +1852,153 @@ class ServerAPI(object): raise ServerError("Failed to create/update dependency") return response.data + def get_dependency_packages(self): + """Information about dependency packages on server. + + To download dependency package, use 'download_dependency_package' + method and pass in 'filename'. + + Example data structure: + { + "packages": [ + { + "filename": str, + "platform": str, + "checksum": str, + "checksumAlgorithm": str, + "size": int, + "sources": list[dict[str, Any]], + "supportedAddons": dict[str, str], + "pythonModules": dict[str, str] + } + ] + } + + Returns: + dict[str, Any]: Information about dependency packages known for + server. + """ + + result = self.get("desktop/dependency_packages") + result.raise_for_status() + return result.data + + def create_dependency_package( + self, + filename, + python_modules, + source_addons, + installer_version, + checksum, + checksum_algorithm, + file_size, + sources=None, + platform_name=None, + ): + """Create dependency package on server. + + The package will be created on a server, it is also required to upload + the package archive file (using 'upload_dependency_package'). + + Args: + filename (str): Filename of dependency package. + python_modules (dict[str, str]): Python modules in dependency + package. + '{"": "", ...}' + source_addons (dict[str, str]): Name of addons for which is + dependency package created. + '{"": "", ...}' + installer_version (str): Version of installer for which was + package created. + checksum (str): Checksum of archive file where dependencies are. + checksum_algorithm (str): Algorithm used to calculate checksum. + file_size (Optional[int]): Size of file. + sources (Optional[list[dict[str, Any]]]): Information about + sources from where it is possible to get file. + platform_name (Optional[str]): Name of platform for which is + dependency package targeted. Default value is + current platform. + """ + + post_body = { + "filename": filename, + "pythonModules": python_modules, + "sourceAddons": source_addons, + "installerVersion": installer_version, + "checksum": checksum, + "checksumAlgorithm": checksum_algorithm, + "size": file_size, + "platform": platform_name or platform.system().lower(), + } + if sources: + post_body["sources"] = sources + + response = self.post("desktop/dependency_packages", **post_body) + response.raise_for_status() + + def update_dependency_package(self, filename, sources): + """Update dependency package metadata on server. + + Args: + filename (str): Filename of dependency package. + sources (list[dict[str, Any]]): Information about + sources from where it is possible to get file. Fully replaces + existing sources. + """ + + response = self.patch( + "desktop/dependency_packages/{}".format(filename), + sources=sources + ) + response.raise_for_status() + + def delete_dependency_package(self, filename, platform_name=None): + """Remove dependency package for specific platform. + + Args: + filename (str): Filename of dependency package. Or name of package + for server version 0.2.0 or lower. + platform_name (Optional[str]): Which platform of the package + should be removed. Current platform is used if not passed. + Deprecated since version 0.2.1 + """ + + major, minor, patch, _, _ = self.server_version_tuple + if major == 0 and (minor < 2 or (minor == 2 and patch < 1)): + if platform_name is None: + platform_name = platform.system().lower() + url = "dependencies/{}/{}".format(filename, platform_name) + else: + url = "desktop/dependency_packages/{}".format(filename) + + response = self.delete(url) + if response.status != 200: + raise ServerError("Failed to delete dependency file") + return response.data + def download_dependency_package( self, - package_name, + src_filename, dst_directory, - filename, + dst_filename, platform_name=None, chunk_size=None, progress=None, ): """Download dependency package from server. - This method requires to have authorized token available. The package is - only downloaded. + This method requires to have authorized token available. The package + is only downloaded. Args: - package_name (str): Name of package to download. + src_filename (str): Filename of dependency pacakge. + For server version 0.2.0 and lower it is name of package + to download. dst_directory (str): Where the file should be downloaded. - filename (str): Name of destination filename. + dst_filename (str): Name of destination filename. platform_name (Optional[str]): Name of platform for which the dependency package is targeted. Default value is - current platform. + current platform. Deprecated since server version 0.2.1. chunk_size (Optional[int]): Download chunk size. progress (Optional[TransferProgress]): Object that gives ability to track download progress. @@ -1673,12 +2006,18 @@ class ServerAPI(object): Returns: str: Filepath to downloaded file. """ - if platform_name is None: - platform_name = platform.system().lower() - package_filepath = os.path.join(dst_directory, filename) + major, minor, patch, _, _ = self.server_version_tuple + if major == 0 and (minor < 2 or (minor == 2 and patch < 1)): + if platform_name is None: + platform_name = platform.system().lower() + url = "dependencies/{}/{}".format(src_filename, platform_name) + else: + url = "desktop/dependency_packages/{}".format(src_filename) + + package_filepath = os.path.join(dst_directory, dst_filename) self.download_file( - "dependencies/{}/{}".format(package_name, platform_name), + url, package_filepath, chunk_size=chunk_size, progress=progress @@ -1686,47 +2025,170 @@ class ServerAPI(object): return package_filepath def upload_dependency_package( - self, filepath, package_name, platform_name=None, progress=None + self, src_filepath, dst_filename, platform_name=None, progress=None ): """Upload dependency package to server. Args: - filepath (str): Path to a package file. - package_name (str): Name of package. Must be unique. + src_filepath (str): Path to a package file. + dst_filename (str): Dependency package filename or name of package + for server version 0.2.0 or lower. Must be unique. platform_name (Optional[str]): For which platform is the - package targeted. + package targeted. Deprecated since server version 0.2.1. progress (Optional[TransferProgress]): Object to keep track about upload state. """ - if platform_name is None: - platform_name = platform.system().lower() + major, minor, patch, _, _ = self.server_version_tuple + if major == 0 and (minor < 2 or (minor == 2 and patch < 1)): + if platform_name is None: + platform_name = platform.system().lower() + url = "dependencies/{}/{}".format(dst_filename, platform_name) + else: + url = "desktop/dependency_packages/{}".format(dst_filename) - self.upload_file( - "dependencies/{}/{}".format(package_name, platform_name), - filepath, - progress=progress - ) + self.upload_file(url, src_filepath, progress=progress) - def delete_dependency_package(self, package_name, platform_name=None): - """Remove dependency package for specific platform. + def create_dependency_package_basename(self, platform_name=None): + """Create basename for dependency package file. Args: - package_name (str): Name of package to remove. - platform_name (Optional[str]): Which platform of the package - should be removed. Current platform is used if not passed. + platform_name (Optional[str]): Name of platform for which the + bundle is targeted. Default value is current platform. + + Returns: + str: Dependency package name with timestamp and platform. """ if platform_name is None: platform_name = platform.system().lower() - response = self.delete( - "dependencies/{}/{}".format(package_name, platform_name), - ) - if response.status != 200: - raise ServerError("Failed to delete dependency file") + now_date = datetime.datetime.now() + time_stamp = now_date.strftime("%y%m%d%H%M") + return "ayon_{}_{}".format(time_stamp, platform_name) + + def get_bundles(self): + """Server bundles with basic information. + + Example output: + { + "bundles": [ + { + "name": "my_bundle", + "createdAt": "2023-06-12T15:37:02.420260", + "installerVersion": "1.0.0", + "addons": { + "core": "1.2.3" + }, + "dependencyPackages": { + "windows": "a_windows_package123.zip", + "linux": "a_linux_package123.zip", + "darwin": "a_mac_package123.zip" + }, + "isProduction": False, + "isStaging": False + } + ], + "productionBundle": "my_bundle", + "stagingBundle": "test_bundle" + } + + Returns: + dict[str, Any]: Server bundles with basic information. + """ + + response = self.get("desktop/bundles") + response.raise_for_status() return response.data + def create_bundle( + self, + name, + addon_versions, + installer_version, + dependency_packages=None, + is_production=None, + is_staging=None + ): + """Create bundle on server. + + Bundle cannot be changed once is created. Only isProduction, isStaging + and dependency packages can change after creation. + + Args: + name (str): Name of bundle. + addon_versions (dict[str, str]): Addon versions. + installer_version (Union[str, None]): Installer version. + dependency_packages (Optional[dict[str, str]]): Dependency + package names. Keys are platform names and values are name of + packages. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + """ + + body = { + "name": name, + "installerVersion": installer_version, + "addons": addon_versions, + } + for key, value in ( + ("dependencyPackages", dependency_packages), + ("isProduction", is_production), + ("isStaging", is_staging), + ): + if value is not None: + body[key] = value + + response = self.post("desktop/bundles", **body) + response.raise_for_status() + + def update_bundle( + self, + bundle_name, + dependency_packages=None, + is_production=None, + is_staging=None + ): + """Update bundle on server. + + Dependency packages can be update only for single platform. Others + will be left untouched. Use 'None' value to unset dependency package + from bundle. + + Args: + bundle_name (str): Name of bundle. + dependency_packages (Optional[dict[str, str]]): Dependency pacakge + names that should be used with the bundle. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + """ + + body = { + key: value + for key, value in ( + ("dependencyPackages", dependency_packages), + ("isProduction", is_production), + ("isStaging", is_staging), + ) + if value is not None + } + response = self.patch( + "desktop/bundles/{}".format(bundle_name), **body + ) + response.raise_for_status() + + def delete_bundle(self, bundle_name): + """Delete bundle from server. + + Args: + bundle_name (str): Name of bundle to delete. + """ + + response = self.delete("desktop/bundles/{}".format(bundle_name)) + response.raise_for_status() + # Anatomy presets def get_project_anatomy_presets(self): """Anatomy presets available on server. @@ -2150,7 +2612,7 @@ class ServerAPI(object): project_name (str): Name of project. Returns: - Union[Dict[str, Any], None]: Project entity data or 'None' if + Union[dict[str, Any], None]: Project entity data or 'None' if project was not found. """ @@ -2174,7 +2636,7 @@ class ServerAPI(object): are returned if 'None' is passed. Returns: - Generator[Dict[str, Any]]: Available projects. + Generator[dict[str, Any]]: Available projects. """ for project_name in self.get_project_names(active, library): @@ -2235,7 +2697,7 @@ class ServerAPI(object): are returned if 'None' is passed. Returns: - List[str]: List of available project names. + list[str]: List of available project names. """ query_keys = {} @@ -2276,7 +2738,7 @@ class ServerAPI(object): not explicitly set on entity will have 'None' value. Returns: - Generator[Dict[str, Any]]: Queried projects. + Generator[dict[str, Any]]: Queried projects. """ if fields is None: @@ -2316,7 +2778,7 @@ class ServerAPI(object): not explicitly set on entity will have 'None' value. Returns: - Union[Dict[str, Any], None]: Project entity data or None + Union[dict[str, Any], None]: Project entity data or None if project was not found. """ @@ -3112,7 +3574,7 @@ class ServerAPI(object): not explicitly set on entity will have 'None' value. Returns: - Generator[Dict[str, Any]]: Queried version entities. + Generator[dict[str, Any]]: Queried version entities. """ if not fields: @@ -3570,7 +4032,7 @@ class ServerAPI(object): not explicitly set on entity will have 'None' value. Returns: - Generator[Dict[str, Any]]: Queried representation entities. + Generator[dict[str, Any]]: Queried representation entities. """ if not fields: @@ -4253,7 +4715,7 @@ class ServerAPI(object): ValueError: When project name already exists. Returns: - Dict[str, Any]: Created project entity. + dict[str, Any]: Created project entity. """ if self.get_project(project_name): @@ -4286,6 +4748,70 @@ class ServerAPI(object): return self.get_project(project_name) + def update_project( + self, + project_name, + library=None, + folder_types=None, + task_types=None, + link_types=None, + statuses=None, + tags=None, + config=None, + attrib=None, + data=None, + active=None, + project_code=None, + **changes + ): + """Update project entity on server. + + Args: + project_name (str): Name of project. + library (Optional[bool]): Change library state. + folder_types (Optional[list[dict[str, Any]]]): Folder type + definitions. + task_types (Optional[list[dict[str, Any]]]): Task type + definitions. + link_types (Optional[list[dict[str, Any]]]): Link type + definitions. + statuses (Optional[list[dict[str, Any]]]): Status definitions. + tags (Optional[list[dict[str, Any]]]): List of tags available to + set on entities. + config (Optional[dict[dict[str, Any]]]): Project anatomy config + with templates and roots. + attrib (Optional[dict[str, Any]]): Project attributes to change. + data (Optional[dict[str, Any]]): Custom data of a project. This + value will 100% override project data. + active (Optional[bool]): Change active state of a project. + project_code (Optional[str]): Change project code. Not recommended + during production. + **changes: Other changed keys based on Rest API documentation. + """ + + changes.update({ + key: value + for key, value in ( + ("library", library), + ("folderTypes", folder_types), + ("taskTypes", task_types), + ("linkTypes", link_types), + ("statuses", statuses), + ("tags", tags), + ("config", config), + ("attrib", attrib), + ("data", data), + ("active", active), + ("code", project_code), + ) + if value is not None + }) + response = self.patch( + "projects/{}".format(project_name), + **changes + ) + response.raise_for_status() + def delete_project(self, project_name): """Delete project from server. diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index d464469cf7..1a1769eeb8 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.2.0" +__version__ = "0.2.1" From 8ff78502dcd7d5f8b197c18e4f84217246d3db6e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 13 Jun 2023 17:26:55 +0200 Subject: [PATCH 247/446] make fusion 'ocio' conversion optional --- openpype/settings/ayon_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index d2a478e259..94417a2045 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -424,7 +424,7 @@ def _convert_fusion_project_settings(ayon_settings, output): ayon_ocio_setting["configFilePath"] = paths ayon_imageio_fusion["ocio"] = ayon_ocio_setting - else: + elif "ocio" in ayon_imageio_fusion: paths = ayon_imageio_fusion["ocio"].pop("configFilePath") for key, value in tuple(paths.items()): new_value = [] From cccce4d1fa2b9f8c5a5eda52f0e9ee542b253d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 23 Jun 2023 10:55:43 +0200 Subject: [PATCH 248/446] Unreal: get current project settings not using unreal project name (#5170) * :bug: use current project settings, not unreal project name * :bug: fix from where the project name is taken * Update openpype/hosts/unreal/hooks/pre_workfile_preparation.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/hosts/unreal/hooks/pre_workfile_preparation.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../unreal/hooks/pre_workfile_preparation.py | 1 + openpype/hosts/unreal/lib.py | 30 +++++++++++------- openpype/hosts/unreal/ue_workers.py | 31 +++++++++++++------ 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index f01609d314..760d55077a 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -111,6 +111,7 @@ class UnrealPrelaunchHook(PreLaunchHook): ue_project_worker = UEProjectGenerationWorker() ue_project_worker.setup( engine_version, + self.data["project_name"], unreal_project_name, engine_path, project_dir diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 97771472cf..67e7891344 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -1,17 +1,16 @@ # -*- coding: utf-8 -*- """Unreal launching and project tools.""" +import json import os import platform -import json - +import re +import subprocess +from collections import OrderedDict +from distutils import dir_util +from pathlib import Path from typing import List -from distutils import dir_util -import subprocess -import re -from pathlib import Path -from collections import OrderedDict from openpype.settings import get_project_settings @@ -179,6 +178,7 @@ def _parse_launcher_locations(install_json_path: str) -> dict: def create_unreal_project(project_name: str, + unreal_project_name: str, ue_version: str, pr_dir: Path, engine_path: Path, @@ -193,7 +193,8 @@ def create_unreal_project(project_name: str, folder and enable this plugin. Args: - project_name (str): Name of the project. + project_name (str): Name of the project in AYON. + unreal_project_name (str): Name of the project in Unreal. ue_version (str): Unreal engine version (like 4.23). pr_dir (Path): Path to directory where project will be created. engine_path (Path): Path to Unreal Engine installation. @@ -211,8 +212,12 @@ def create_unreal_project(project_name: str, Returns: None + Deprecated: + since 3.16.0 + """ env = env or os.environ + preset = get_project_settings(project_name)["unreal"]["project_setup"] ue_id = ".".join(ue_version.split(".")[:2]) # get unreal engine identifier @@ -230,7 +235,7 @@ def create_unreal_project(project_name: str, ue_editor_exe: Path = get_editor_exe_path(engine_path, ue_version) cmdlet_project: Path = get_path_to_cmdlet_project(ue_version) - project_file = pr_dir / f"{project_name}.uproject" + project_file = pr_dir / f"{unreal_project_name}.uproject" print("--- Generating a new project ...") commandlet_cmd = [f'{ue_editor_exe.as_posix()}', @@ -251,8 +256,9 @@ def create_unreal_project(project_name: str, return_code = gen_process.wait() if return_code and return_code != 0: - raise RuntimeError(f'Failed to generate \'{project_name}\' project! ' - f'Exited with return code {return_code}') + raise RuntimeError( + (f"Failed to generate '{unreal_project_name}' project! " + f"Exited with return code {return_code}")) print("--- Project has been generated successfully.") @@ -282,7 +288,7 @@ def create_unreal_project(project_name: str, subprocess.run(command1) command2 = [u_build_tool.as_posix(), - f"-ModuleWithSuffix={project_name},3555", arch, + f"-ModuleWithSuffix={unreal_project_name},3555", arch, "Development", "-TargetType=Editor", f'-Project={project_file}', f'{project_file}', diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index 2b7e1375e6..3a0f976957 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -3,16 +3,17 @@ import os import platform import re import subprocess +import tempfile from distutils import dir_util +from distutils.dir_util import copy_tree from pathlib import Path from typing import List, Union -import tempfile -from distutils.dir_util import copy_tree - -import openpype.hosts.unreal.lib as ue_lib from qtpy import QtCore +import openpype.hosts.unreal.lib as ue_lib +from openpype.settings import get_project_settings + def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)): match = re.search(r"\[[1-9]+/[0-9]+]", line) @@ -54,24 +55,36 @@ class UEProjectGenerationWorker(QtCore.QObject): dev_mode = False def setup(self, ue_version: str, - project_name, + project_name: str, + unreal_project_name, engine_path: Path, project_dir: Path, dev_mode: bool = False, env: dict = None): + """Set the worker with necessary parameters. + + Args: + ue_version (str): Unreal Engine version. + project_name (str): Name of the project in AYON. + unreal_project_name (str): Name of the project in Unreal. + engine_path (Path): Path to the Unreal Engine. + project_dir (Path): Path to the project directory. + dev_mode (bool, optional): Whether to run the project in dev mode. + Defaults to False. + env (dict, optional): Environment variables. Defaults to None. + + """ self.ue_version = ue_version self.project_dir = project_dir self.env = env or os.environ - preset = ue_lib.get_project_settings( - project_name - )["unreal"]["project_setup"] + preset = get_project_settings(project_name)["unreal"]["project_setup"] if dev_mode or preset["dev_mode"]: self.dev_mode = True - self.project_name = project_name + self.project_name = unreal_project_name self.engine_path = engine_path def run(self): From 8b69159033370779abd32593e28757f0f0f12f9d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 23 Jun 2023 18:39:58 +0200 Subject: [PATCH 249/446] fix missing 'active' argument in 'get_last_versions' --- openpype/client/server/entities.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index fee503297b..9579f13add 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -99,6 +99,7 @@ def _get_versions( hero=True, standard=True, latest=None, + active=None, fields=None ): con = get_server_api_connection() @@ -119,6 +120,7 @@ def _get_versions( hero, standard, latest, + active=active, fields=fields ) @@ -364,7 +366,7 @@ def get_hero_versions( ) -def get_last_versions(project_name, subset_ids, fields=None): +def get_last_versions(project_name, subset_ids, active=None, fields=None): if fields: fields = set(fields) fields.add("parent") @@ -374,6 +376,7 @@ def get_last_versions(project_name, subset_ids, fields=None): subset_ids=subset_ids, latest=True, hero=False, + active=active, fields=fields ) return { From 546187e43e52d8e265cc5a4e12a0b729625a3c2c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 28 Jun 2023 16:12:10 +0200 Subject: [PATCH 250/446] AYON: OpenPype as server addon (#5199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added openpype server addon creation logic * Better readme Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> * added docstring to 'find_files_in_subdir' * added some type notations * add whitespace around assignment * move pyproject toml to client subfolder and fix its location in private dir * Fix whitespace --------- Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- .gitignore | 1 + server_addon/README.md | 21 +++++ server_addon/client/pyproject.toml | 24 +++++ server_addon/create_ayon_addon.py | 140 +++++++++++++++++++++++++++++ server_addon/server/__init__.py | 9 ++ 5 files changed, 195 insertions(+) create mode 100644 server_addon/README.md create mode 100644 server_addon/client/pyproject.toml create mode 100644 server_addon/create_ayon_addon.py create mode 100644 server_addon/server/__init__.py diff --git a/.gitignore b/.gitignore index 50f52f65a3..e5019a4e74 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ Temporary Items ########### /build /dist/ +/server_addon/package/* /vendor/bin/* /vendor/python/* diff --git a/server_addon/README.md b/server_addon/README.md new file mode 100644 index 0000000000..fa9a6001d2 --- /dev/null +++ b/server_addon/README.md @@ -0,0 +1,21 @@ +# OpenPype addon for AYON server +Convert openpype into AYON addon which can be installed on AYON server. The versioning of the addon is following versioning of OpenPype. + +## Intro +OpenPype is transitioning to AYON, a dedicated server with its own database, moving away from MongoDB. During this transition period, OpenPype will remain compatible with both MongoDB and AYON. However, we will gradually update the codebase to align with AYON's data structure and separate individual components into addons. + +Currently, OpenPype has an AYON mode, which means it utilizes the AYON server instead of MongoDB through conversion utilities. Initially, we added the AYON executable alongside the OpenPype executables to enable AYON mode. While this approach worked, updating to new code versions would require a complete reinstallation. To address this, we have decided to create a new repository specifically for the base desktop application logic, which we currently refer to as the AYON Launcher. This Launcher will replace the executables generated by the OpenPype build and convert the OpenPype code into a server addon, resulting in smaller updates. + +Since the implementation of the AYON Launcher is not yet fully completed, we will maintain both methods of starting AYON mode for now. Once the AYON Launcher is finished, we will remove the AYON executables from the OpenPype codebase entirely. + +During this transitional period, the AYON Launcher addon will be a requirement as the entry point for using the AYON Launcher. + +## How to start +There is a `create_ayon_addon.py` python file which contains logic how to create server addon from OpenPype codebase. Just run the code. +```shell +./.poetry/bin/poetry run python ./server_addon/create_ayon_addon.py +``` + +It will create directory `./package/openpype//*` folder with all files necessary for AYON server. You can then copy `./package/openpype/` to server addons, or zip the folder and upload it to AYON server. Restart server to update addons information, add the addon version to server bundle and set the bundle for production or staging usage. + +Once addon is on server and is enabled, you can just run AYON launcher. Content will be downloaded and used automatically. diff --git a/server_addon/client/pyproject.toml b/server_addon/client/pyproject.toml new file mode 100644 index 0000000000..0d49cc7425 --- /dev/null +++ b/server_addon/client/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name="openpype" +description="OpenPype addon for AYON server." + +[tool.poetry.dependencies] +python = ">=3.9.1,<3.10" +aiohttp_json_rpc = "*" # TVPaint server +aiohttp-middlewares = "^2.0.0" +wsrpc_aiohttp = "^3.1.1" # websocket server +clique = "1.6.*" +shotgun_api3 = {git = "https://github.com/shotgunsoftware/python-api.git", rev = "v3.3.3"} +gazu = "^0.9.3" +google-api-python-client = "^1.12.8" # sync server google support (should be separate?) +jsonschema = "^2.6.0" +log4mongo = "^1.7" +pathlib2= "^2.3.5" # deadline submit publish job only (single place, maybe not needed?) +pyblish-base = "^1.8.11" +pynput = "^1.7.2" # Timers manager - TODO replace +"Qt.py" = "^1.3.3" +qtawesome = "0.7.3" +speedcopy = "^2.1" +slack-sdk = "^3.6.0" +pysftp = "^0.2.9" +dropbox = "^11.20.0" diff --git a/server_addon/create_ayon_addon.py b/server_addon/create_ayon_addon.py new file mode 100644 index 0000000000..657f416441 --- /dev/null +++ b/server_addon/create_ayon_addon.py @@ -0,0 +1,140 @@ +import os +import re +import shutil +import zipfile +import collections +from pathlib import Path +from typing import Any, Optional, Iterable + +# Patterns of directories to be skipped for server part of addon +IGNORE_DIR_PATTERNS: list[re.Pattern] = [ + re.compile(pattern) + for pattern in { + # Skip directories starting with '.' + r"^\.", + # Skip any pycache folders + "^__pycache__$" + } +] + +# Patterns of files to be skipped for server part of addon +IGNORE_FILE_PATTERNS: list[re.Pattern] = [ + re.compile(pattern) + for pattern in { + # Skip files starting with '.' + # NOTE this could be an issue in some cases + r"^\.", + # Skip '.pyc' files + r"\.pyc$" + } +] + + +def _value_match_regexes(value: str, regexes: Iterable[re.Pattern]) -> bool: + return any( + regex.search(value) + for regex in regexes + ) + + +def find_files_in_subdir( + src_path: str, + ignore_file_patterns: Optional[list[re.Pattern]] = None, + ignore_dir_patterns: Optional[list[re.Pattern]] = None +): + """Find all files to copy in subdirectories of given path. + + All files that match any of the patterns in 'ignore_file_patterns' will + be skipped and any directories that match any of the patterns in + 'ignore_dir_patterns' will be skipped with all subfiles. + + Args: + src_path (str): Path to directory to search in. + ignore_file_patterns (Optional[list[re.Pattern]]): List of regexes + to match files to ignore. + ignore_dir_patterns (Optional[list[re.Pattern]]): List of regexes + to match directories to ignore. + + Returns: + list[tuple[str, str]]: List of tuples with path to file and parent + directories relative to 'src_path'. + """ + + if ignore_file_patterns is None: + ignore_file_patterns = IGNORE_FILE_PATTERNS + + if ignore_dir_patterns is None: + ignore_dir_patterns = IGNORE_DIR_PATTERNS + output: list[tuple[str, str]] = [] + + hierarchy_queue = collections.deque() + hierarchy_queue.append((src_path, [])) + while hierarchy_queue: + item: tuple[str, str] = hierarchy_queue.popleft() + dirpath, parents = item + for name in os.listdir(dirpath): + path = os.path.join(dirpath, name) + if os.path.isfile(path): + if not _value_match_regexes(name, ignore_file_patterns): + items = list(parents) + items.append(name) + output.append((path, os.path.sep.join(items))) + continue + + if not _value_match_regexes(name, ignore_dir_patterns): + items = list(parents) + items.append(name) + hierarchy_queue.append((path, items)) + + return output + + +def main(): + openpype_addon_dir = Path(os.path.dirname(os.path.abspath(__file__))) + server_dir = openpype_addon_dir / "server" + package_root = openpype_addon_dir / "package" + pyproject_path = openpype_addon_dir / "client" / "pyproject.toml" + + root_dir = openpype_addon_dir.parent + openpype_dir = root_dir / "openpype" + version_path = openpype_dir / "version.py" + + # Read version + version_content: dict[str, Any] = {} + with open(str(version_path), "r") as stream: + exec(stream.read(), version_content) + addon_version: str = version_content["__version__"] + + output_dir = package_root / "openpype" / addon_version + private_dir = output_dir / "private" + + # Make sure package dir is empty + if package_root.exists(): + shutil.rmtree(str(package_root)) + # Make sure output dir is created + output_dir.mkdir(parents=True) + + # Copy version + shutil.copy(str(version_path), str(output_dir)) + for subitem in server_dir.iterdir(): + shutil.copy(str(subitem), str(output_dir / subitem.name)) + + # Make sure private dir exists + private_dir.mkdir(parents=True) + + # Copy pyproject.toml + shutil.copy( + str(pyproject_path), + (private_dir / pyproject_path.name) + ) + + # Zip client + zip_filepath = private_dir / "client.zip" + with zipfile.ZipFile(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf: + # Add client code content to zip + for path, sub_path in find_files_in_subdir(str(openpype_dir)): + zipf.write(path, f"{openpype_dir.name}/{sub_path}") + + +if __name__ == "__main__": + main() diff --git a/server_addon/server/__init__.py b/server_addon/server/__init__.py new file mode 100644 index 0000000000..df24c73c76 --- /dev/null +++ b/server_addon/server/__init__.py @@ -0,0 +1,9 @@ +from ayon_server.addons import BaseServerAddon + +from .version import __version__ + + +class OpenPypeAddon(BaseServerAddon): + name = "openpype" + title = "OpenPype" + version = __version__ From 71158356e43b9c3966c313526383a018dd85c31e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 29 Jun 2023 12:17:11 +0200 Subject: [PATCH 251/446] update ayon api to '0.3.0' --- .../vendor/python/common/ayon_api/__init__.py | 3 + .../vendor/python/common/ayon_api/_api.py | 10 + .../python/common/ayon_api/server_api.py | 335 ++++++++++++++---- .../vendor/python/common/ayon_api/version.py | 2 +- 4 files changed, 289 insertions(+), 61 deletions(-) diff --git a/openpype/vendor/python/common/ayon_api/__init__.py b/openpype/vendor/python/common/ayon_api/__init__.py index 6f791972cd..9e0738071f 100644 --- a/openpype/vendor/python/common/ayon_api/__init__.py +++ b/openpype/vendor/python/common/ayon_api/__init__.py @@ -101,6 +101,7 @@ from ._api import ( get_addon_studio_settings, get_addon_project_settings, get_addon_settings, + get_bundle_settings, get_addons_studio_settings, get_addons_project_settings, get_addons_settings, @@ -116,6 +117,7 @@ from ._api import ( get_folder_by_name, get_folder_by_path, get_folders, + get_folders_hierarchy, get_tasks, @@ -279,6 +281,7 @@ __all__ = ( "get_addon_studio_settings", "get_addon_project_settings", "get_addon_settings", + "get_bundle_settings", "get_addons_studio_settings", "get_addons_project_settings", "get_addons_settings", diff --git a/openpype/vendor/python/common/ayon_api/_api.py b/openpype/vendor/python/common/ayon_api/_api.py index f5a3a4787a..82ffdc7527 100644 --- a/openpype/vendor/python/common/ayon_api/_api.py +++ b/openpype/vendor/python/common/ayon_api/_api.py @@ -688,6 +688,11 @@ def get_addon_site_settings(*args, **kwargs): return con.get_addon_site_settings(*args, **kwargs) +def get_bundle_settings(*args, **kwargs): + con = get_server_api_connection() + return con.get_bundle_settings(*args, **kwargs) + + def get_addons_studio_settings(*args, **kwargs): con = get_server_api_connection() return con.get_addons_studio_settings(*args, **kwargs) @@ -723,6 +728,11 @@ def get_folders(*args, **kwargs): return con.get_folders(*args, **kwargs) +def get_folders_hierarchy(*args, **kwargs): + con = get_server_api_connection() + return con.get_folders_hierarchy(*args, **kwargs) + + def get_tasks(*args, **kwargs): con = get_server_api_connection() return con.get_tasks(*args, **kwargs) diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index 8cbb23ce0d..41a5ad2f9c 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -310,9 +310,14 @@ class ServerAPI(object): connection is created from the same machine under same user. client_version (Optional[str]): Version of client application (used in desktop client application). - default_settings_variant (Optional[str]): Settings variant used by - default if a method for settings won't get any (by default is - 'production'). + 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'). + 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. + cert (Optional[str]): Path to certificate file. Looks for env + variable value 'AYON_CERT_FILE' by default. """ def __init__( @@ -321,7 +326,9 @@ class ServerAPI(object): token=None, site_id=None, client_version=None, - default_settings_variant=None + default_settings_variant=None, + ssl_verify=None, + cert=None, ): if not base_url: raise ValueError("Invalid server URL {}".format(str(base_url))) @@ -334,7 +341,23 @@ class ServerAPI(object): self._access_token = token self._site_id = site_id self._client_version = client_version - self._default_settings_variant = default_settings_variant + self._default_settings_variant = ( + default_settings_variant + or "production" + ) + + if ssl_verify is None: + # Custom AYON env variable for CA file or 'True' + # - that should cover most default behaviors in 'requests' + # with 'certifi' + ssl_verify = os.environ.get("AYON_CA_FILE") or True + + if cert is None: + cert = os.environ.get("AYON_CERT_FILE") + + self._ssl_verify = ssl_verify + self._cert = cert + self._access_token_is_service = None self._token_is_valid = None self._server_available = None @@ -374,6 +397,54 @@ class ServerAPI(object): base_url = property(get_base_url) rest_url = property(get_rest_url) + def get_ssl_verify(self): + """Enable ssl verification. + + Returns: + bool: Current state of ssl verification. + """ + + return self._ssl_verify + + def set_ssl_verify(self, ssl_verify): + """Change ssl verification state. + + Args: + ssl_verify (Union[bool, str, None]): Enabled/disable + ssl verification, can be a path to file. + """ + + if self._ssl_verify == ssl_verify: + return + self._ssl_verify = ssl_verify + if self._session is not None: + self._session.verify = ssl_verify + + def get_cert(self): + """Current cert file used for connection to server. + + Returns: + Union[str, None]: Path to cert file. + """ + + return self._cert + + def set_cert(self, cert): + """Change cert file used for connection to server. + + Args: + cert (Union[str, None]): Path to cert file. + """ + + if cert == self._cert: + return + self._cert = cert + if self._session is not None: + self._session.cert = cert + + ssl_verify = property(get_ssl_verify, set_ssl_verify) + cert = property(get_cert, set_cert) + @property def access_token(self): """Access token used for authorization to server. @@ -459,9 +530,13 @@ class ServerAPI(object): as default variant. Args: - variant (Union[str, None]): Settings variant name. + variant (Literal['production', 'staging']): Settings variant name. """ + if variant not in ("production", "staging"): + raise ValueError(( + "Invalid variant name {}. Expected 'production' or 'staging'" + ).format(variant)) self._default_settings_variant = variant default_settings_variant = property( @@ -545,7 +620,11 @@ class ServerAPI(object): @property def is_server_available(self): if self._server_available is None: - response = requests.get(self._base_url) + response = requests.get( + self._base_url, + cert=self._cert, + verify=self._ssl_verify + ) self._server_available = response.status_code == 200 return self._server_available @@ -596,6 +675,8 @@ class ServerAPI(object): self.validate_token() session = requests.Session() + session.cert = self._cert + session.verify = self._ssl_verify session.headers.update(self.get_headers()) self._session_functions_mapping = { @@ -1964,12 +2045,13 @@ class ServerAPI(object): """ major, minor, patch, _, _ = self.server_version_tuple - if major == 0 and (minor < 2 or (minor == 2 and patch < 1)): + if major == 0 and (minor > 2 or (minor == 2 and patch >= 1)): + url = "desktop/dependency_packages/{}".format(filename) + else: + # Backwards compatibility for AYON server 0.2.0 and lower if platform_name is None: platform_name = platform.system().lower() url = "dependencies/{}/{}".format(filename, platform_name) - else: - url = "desktop/dependency_packages/{}".format(filename) response = self.delete(url) if response.status != 200: @@ -2008,12 +2090,13 @@ class ServerAPI(object): """ major, minor, patch, _, _ = self.server_version_tuple - if major == 0 and (minor < 2 or (minor == 2 and patch < 1)): + if major == 0 and (minor > 2 or (minor == 2 and patch >= 1)): + url = "desktop/dependency_packages/{}".format(src_filename) + else: + # Backwards compatibility for AYON server 0.2.0 and lower if platform_name is None: platform_name = platform.system().lower() url = "dependencies/{}/{}".format(src_filename, platform_name) - else: - url = "desktop/dependency_packages/{}".format(src_filename) package_filepath = os.path.join(dst_directory, dst_filename) self.download_file( @@ -2040,12 +2123,13 @@ class ServerAPI(object): """ major, minor, patch, _, _ = self.server_version_tuple - if major == 0 and (minor < 2 or (minor == 2 and patch < 1)): + if major == 0 and (minor > 2 or (minor == 2 and patch >= 1)): + url = "desktop/dependency_packages/{}".format(dst_filename) + else: + # Backwards compatibility for AYON server 0.2.0 and lower if platform_name is None: platform_name = platform.system().lower() url = "dependencies/{}/{}".format(dst_filename, platform_name) - else: - url = "desktop/dependency_packages/{}".format(dst_filename) self.upload_file(url, src_filepath, progress=progress) @@ -2330,17 +2414,17 @@ class ServerAPI(object): ): """Addon studio settings. - Receive studio settings for specific version of an addon. + Receive studio settings for specific version of an addon. - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - variant (Optional[str]): Name of settings variant. By default, - is used 'default_settings_variant' passed on init. + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. - Returns: + Returns: dict[str, Any]: Addon settings. - """ + """ if variant is None: variant = self.default_settings_variant @@ -2378,8 +2462,8 @@ class ServerAPI(object): addon_version (str): Version of addon. project_name (str): Name of project for which the settings are received. - variant (Optional[str]): Name of settings variant. By default, - is used 'production'. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. site_id (Optional[str]): Name of site which is used for site overrides. Is filled with connection 'site_id' attribute if not passed. @@ -2435,8 +2519,8 @@ class ServerAPI(object): project_name (Optional[str]): Name of project for which the settings are received. A studio settings values are received if is 'None'. - variant (Optional[str]): Name of settings variant. By default, - is used 'production'. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. site_id (Optional[str]): Name of site which is used for site overrides. Is filled with connection 'site_id' attribute if not passed. @@ -2486,12 +2570,106 @@ class ServerAPI(object): result.raise_for_status() return result.data - def get_addons_studio_settings(self, variant=None, only_values=True): + def get_bundle_settings( + self, + bundle_name=None, + project_name=None, + variant=None, + site_id=None, + use_site=True + ): + """Get complete set of settings for given data. + + If project is not passed then studio settings are returned. If variant + is not passed 'default_settings_variant' is used. If bundle name is + not passed then current production/staging bundle is used, based on + variant value. + + Output contains addon settings and site settings in single dictionary. + + TODOs: + - test how it behaves if there is not any bundle. + - test how it behaves if there is not any production/staging + bundle. + + Warnings: + For AYON server < 0.3.0 bundle name will be ignored. + + Example output: + { + "addons": [ + { + "name": "addon-name", + "version": "addon-version", + "settings": {...} + "siteSettings": {...} + } + ] + } + + Returns: + dict[str, Any]: All settings for single bundle. + """ + + major, minor, _, _, _ = self.server_version_tuple + query_values = { + key: value + for key, value in ( + ("project_name", project_name), + ("variant", variant or self.default_settings_variant), + ("bundle_name", bundle_name), + ) + if value + } + if use_site: + if not site_id: + site_id = self.site_id + if site_id: + query_values["site_id"] = site_id + + if major == 0 and minor >= 3: + url = "settings" + else: + # Backward compatibility for AYON server < 0.3.0 + url = "settings/addons" + query_values.pop("bundle_name", None) + for new_key, old_key in ( + ("project_name", "project"), + ("site_id", "site"), + ): + if new_key in query_values: + query_values[old_key] = query_values.pop(new_key) + + query = prepare_query_string(query_values) + response = self.get("{}{}".format(url, query)) + response.raise_for_status() + return response.data + + def get_addons_studio_settings( + self, + bundle_name=None, + variant=None, + site_id=None, + use_site=True, + only_values=True + ): """All addons settings in one bulk. + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + Args: - variant (Optional[Literal[production, staging]]): Variant of - settings. By default, is used 'production'. + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Id of site for which want to receive + site overrides. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. only_values (Optional[bool]): Output will contain only settings values without metadata about addons. @@ -2499,20 +2677,28 @@ class ServerAPI(object): dict[str, Any]: Settings of all addons on server. """ - query_values = {} - if variant: - query_values["variant"] = variant - query = prepare_query_string(query_values) - response = self.get("settings/addons{}".format(query)) - response.raise_for_status() - output = response.data + output = self.get_bundle_settings( + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site + ) if only_values: - output = output["settings"] + major, minor, patch, _, _ = self.server_version_tuple + if major == 0 and minor >= 3: + output = { + addon["name"]: addon["settings"] + for addon in output["addons"] + } + else: + # Backward compatibility for AYON server < 0.3.0 + output = output["settings"] return output def get_addons_project_settings( self, project_name, + bundle_name=None, variant=None, site_id=None, use_site=True, @@ -2531,11 +2717,18 @@ class ServerAPI(object): argument which is by default set to 'True'. In that case output contains only value of 'settings' key. + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + Args: project_name (str): Name of project for which are settings received. - variant (Optional[Literal[production, staging]]): Variant of - settings. By default, is used 'production'. + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. site_id (Optional[str]): Id of site for which want to receive site overrides. use_site (bool): To force disable option of using site overrides @@ -2549,27 +2742,31 @@ class ServerAPI(object): project. """ - query_values = { - "project": project_name - } - if variant: - query_values["variant"] = variant + if not project_name: + raise ValueError("Project name must be passed.") - if use_site: - if not site_id: - site_id = self.default_settings_variant - if site_id: - query_values["site"] = site_id - query = prepare_query_string(query_values) - response = self.get("settings/addons{}".format(query)) - response.raise_for_status() - output = response.data + output = self.get_bundle_settings( + project_name=project_name, + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site + ) if only_values: - output = output["settings"] + major, minor, patch, _, _ = self.server_version_tuple + if major == 0 and minor >= 3: + output = { + addon["name"]: addon["settings"] + for addon in output["addons"] + } + else: + # Backward compatibility for AYON server < 0.3.0 + output = output["settings"] return output def get_addons_settings( self, + bundle_name=None, project_name=None, variant=None, site_id=None, @@ -2581,11 +2778,18 @@ class ServerAPI(object): Based on 'project_name' will receive studio settings or project settings. In case project is not passed is 'site_id' ignored. + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + Args: + bundle_name (Optional[str]): Name of bundle for which should be + settings received. project_name (Optional[str]): Name of project for which should be settings received. - variant (Optional[Literal[production, staging]]): Settings variant. - By default, is used 'production'. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. site_id (Optional[str]): Id of site for which want to receive site overrides. use_site (Optional[bool]): To force disable option of using site @@ -2596,10 +2800,21 @@ class ServerAPI(object): """ if project_name is None: - return self.get_addons_studio_settings(variant, only_values) + return self.get_addons_studio_settings( + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site, + only_values=only_values + ) return self.get_addons_project_settings( - project_name, variant, site_id, use_site, only_values + project_name=project_name, + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site, + only_values=only_values ) # Entity getters diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index 1a1769eeb8..6c6b912848 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.2.1" +__version__ = "0.3.0" From bc7c40975eeede314fd9ba051d880794c1a9b6df Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 30 Jun 2023 08:33:14 +0200 Subject: [PATCH 252/446] update ayon_api to 0.3.1 --- .../python/common/ayon_api/server_api.py | 78 ++++++++++--------- .../vendor/python/common/ayon_api/version.py | 2 +- 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index 41a5ad2f9c..c702101f2b 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -1964,6 +1964,24 @@ class ServerAPI(object): result.raise_for_status() return result.data + def _get_dependency_package_route( + self, filename=None, platform_name=None + ): + major, minor, patch, _, _ = self.server_version_tuple + if major == 0 and (minor > 2 or (minor == 2 and patch >= 1)): + base = "desktop/dependency_packages" + if not filename: + return base + return "{}/{}".format(base, filename) + + # Backwards compatibility for AYON server 0.2.0 and lower + if platform_name is None: + platform_name = platform.system().lower() + base = "dependencies" + if not filename: + return base + return "{}/{}/{}".format(base, filename, platform_name) + def create_dependency_package( self, filename, @@ -2014,7 +2032,8 @@ class ServerAPI(object): if sources: post_body["sources"] = sources - response = self.post("desktop/dependency_packages", **post_body) + route = self._get_dependency_package_route() + response = self.post(route, **post_body) response.raise_for_status() def update_dependency_package(self, filename, sources): @@ -2028,7 +2047,7 @@ class ServerAPI(object): """ response = self.patch( - "desktop/dependency_packages/{}".format(filename), + self._get_dependency_package_route(filename), sources=sources ) response.raise_for_status() @@ -2044,16 +2063,8 @@ class ServerAPI(object): Deprecated since version 0.2.1 """ - major, minor, patch, _, _ = self.server_version_tuple - if major == 0 and (minor > 2 or (minor == 2 and patch >= 1)): - url = "desktop/dependency_packages/{}".format(filename) - else: - # Backwards compatibility for AYON server 0.2.0 and lower - if platform_name is None: - platform_name = platform.system().lower() - url = "dependencies/{}/{}".format(filename, platform_name) - - response = self.delete(url) + route = self._get_dependency_package_route(filename, platform_name) + response = self.delete(route) if response.status != 200: raise ServerError("Failed to delete dependency file") return response.data @@ -2089,18 +2100,10 @@ class ServerAPI(object): str: Filepath to downloaded file. """ - major, minor, patch, _, _ = self.server_version_tuple - if major == 0 and (minor > 2 or (minor == 2 and patch >= 1)): - url = "desktop/dependency_packages/{}".format(src_filename) - else: - # Backwards compatibility for AYON server 0.2.0 and lower - if platform_name is None: - platform_name = platform.system().lower() - url = "dependencies/{}/{}".format(src_filename, platform_name) - + route = self._get_dependency_package_route(src_filename, platform_name) package_filepath = os.path.join(dst_directory, dst_filename) self.download_file( - url, + route, package_filepath, chunk_size=chunk_size, progress=progress @@ -2122,16 +2125,8 @@ class ServerAPI(object): upload state. """ - major, minor, patch, _, _ = self.server_version_tuple - if major == 0 and (minor > 2 or (minor == 2 and patch >= 1)): - url = "desktop/dependency_packages/{}".format(dst_filename) - else: - # Backwards compatibility for AYON server 0.2.0 and lower - if platform_name is None: - platform_name = platform.system().lower() - url = "dependencies/{}/{}".format(dst_filename, platform_name) - - self.upload_file(url, src_filepath, progress=progress) + route = self._get_dependency_package_route(dst_filename, platform_name) + self.upload_file(route, src_filepath, progress=progress) def create_dependency_package_basename(self, platform_name=None): """Create basename for dependency package file. @@ -2151,6 +2146,14 @@ class ServerAPI(object): time_stamp = now_date.strftime("%y%m%d%H%M") return "ayon_{}_{}".format(time_stamp, platform_name) + def _get_bundles_route(self): + major, minor, patch, _, _ = self.server_version_tuple + # Backwards compatibility for AYON server 0.3.0 + # - first version where bundles were available + if major == 0 and minor == 3 and patch == 0: + return "desktop/bundles" + return "bundles" + def get_bundles(self): """Server bundles with basic information. @@ -2181,7 +2184,7 @@ class ServerAPI(object): dict[str, Any]: Server bundles with basic information. """ - response = self.get("desktop/bundles") + response = self.get(self._get_bundles_route()) response.raise_for_status() return response.data @@ -2224,7 +2227,7 @@ class ServerAPI(object): if value is not None: body[key] = value - response = self.post("desktop/bundles", **body) + response = self.post(self._get_bundles_route(), **body) response.raise_for_status() def update_bundle( @@ -2259,7 +2262,8 @@ class ServerAPI(object): if value is not None } response = self.patch( - "desktop/bundles/{}".format(bundle_name), **body + "{}/{}".format(self._get_bundles_route(), bundle_name), + **body ) response.raise_for_status() @@ -2270,7 +2274,9 @@ class ServerAPI(object): bundle_name (str): Name of bundle to delete. """ - response = self.delete("desktop/bundles/{}".format(bundle_name)) + response = self.delete( + "{}/{}".format(self._get_bundles_route(), bundle_name) + ) response.raise_for_status() # Anatomy presets diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index 6c6b912848..e9dd1f445a 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.3.0" +__version__ = "0.3.1" From 551b51904bd6bdd2dae491e3287a931c0460afd8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 30 Jun 2023 08:50:43 +0200 Subject: [PATCH 253/446] fix conversion of Others templates --- openpype/client/server/conversion_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index b5bd755470..dc95bbeda5 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -140,6 +140,9 @@ def _template_replacements_to_v3(template): def _convert_template_item(template): + # Others won't have 'directory' + if "directory" not in template: + return folder = _template_replacements_to_v3(template.pop("directory")) template["folder"] = folder template["file"] = _template_replacements_to_v3(template["file"]) From 301375c4010e7899a9082c2dd53f1a2fd6ab8ec2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Jul 2023 10:43:29 +0200 Subject: [PATCH 254/446] General: Runtime dependencies (#5206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * moved opentimlineio to runtime dependencies * fix darwin fetch thirdparty * Fix print Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> * fix comment in pyproject toml --------- Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- poetry.lock | 51 ---------------------------------- pyproject.toml | 10 +++---- tools/fetch_thirdparty_libs.py | 15 +++++----- 3 files changed, 13 insertions(+), 63 deletions(-) diff --git a/poetry.lock b/poetry.lock index d8bc441875..5621d39988 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1894,46 +1894,6 @@ files = [ [package.dependencies] setuptools = "*" -[[package]] -name = "opentimelineio" -version = "0.14.1" -description = "Editorial interchange format and API" -category = "main" -optional = false -python-versions = ">2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.9.0" -files = [ - {file = "OpenTimelineIO-0.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:d5466742d1de323e922965e64ca7099f6dd756774d5f8b404a11d6ec6e7c5fe0"}, - {file = "OpenTimelineIO-0.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:3f5187eb0cd8f607bfcc5c1d58ce878734975a0a6a91360a2605ad831198ed89"}, - {file = "OpenTimelineIO-0.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a2b64bf817d3065f7302c748bcc1d5938971e157c42e67fcb4e5e3612358813b"}, - {file = "OpenTimelineIO-0.14.1-cp27-cp27m-win32.whl", hash = "sha256:4cde33ea83ba041332bae55474fc155219871396b82031dd54d3e857973805b6"}, - {file = "OpenTimelineIO-0.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:d5dc153867c688ad4f39cbac78eda069cfe4f17376d9444d202f8073efa6cbd4"}, - {file = "OpenTimelineIO-0.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:e07390dd1e0f82e5a5880ef2d498cbcbf482b4e5bfb4b9026342578a2fad358d"}, - {file = "OpenTimelineIO-0.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4c1c522df397536c7620d44e32302165a9ef9bbbf0de83a5a0621f0a75047cc9"}, - {file = "OpenTimelineIO-0.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e368a1d64366e3fdf1eadd10077a135833fdc893ff65f8dc43a91254cb7ee6fa"}, - {file = "OpenTimelineIO-0.14.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:cf2cd94d11d0ae0fc78418cc0d17f2fe3bf85598b9b109f98b2301272a87bff5"}, - {file = "OpenTimelineIO-0.14.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7af41f43ef72fbf3c0ae2e47cabd7715eb348726c9e5e430ab36ce2357181cf4"}, - {file = "OpenTimelineIO-0.14.1-cp37-cp37m-win32.whl", hash = "sha256:55dbb859d16535ba5dab8a66a78aef8db55f030d771b6e5b91e94241b6db65bd"}, - {file = "OpenTimelineIO-0.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08eaef8fbc423c25e94e189eb788c92c16916ae74d16ebcab34ba889e980c6ad"}, - {file = "OpenTimelineIO-0.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:10b34a6997d6d6edb9b8a1c93718a1e90e8202d930559cdce2ad369e0473327f"}, - {file = "OpenTimelineIO-0.14.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c6b44986da8c7a64f8f549795279f0af05ec875a425d11600585dab0b3269ec2"}, - {file = "OpenTimelineIO-0.14.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:45e1774d9f7215190a7c1e5b70dfc237f4a03b79b0539902d9ec8074707450f9"}, - {file = "OpenTimelineIO-0.14.1-cp38-cp38-win32.whl", hash = "sha256:1ee0e72320309b8dedf0e2f40fc2b8d3dd2c854db0aba28a84a038d7177a1208"}, - {file = "OpenTimelineIO-0.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:bd58e9fdc765623e160ab3ec32e9199bcb3906a6f3c06cca7564fbb7c18d2d28"}, - {file = "OpenTimelineIO-0.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f8d6e15f793577de59cc01e49600898ab12dbdc260dbcba83936c00965f0090a"}, - {file = "OpenTimelineIO-0.14.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:50644c5e43076a3717b77645657545d0be19376ecb4c6f2e4103670052d726d4"}, - {file = "OpenTimelineIO-0.14.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:a44f77fb5dbfd60d992ac2acc6782a7b0a26452db3a069425b8bd73b2f3bb336"}, - {file = "OpenTimelineIO-0.14.1-cp39-cp39-win32.whl", hash = "sha256:63fb0d1258f490bcebf6325067db64a0f0dc405b8b905ee2bb625f04d04a8082"}, - {file = "OpenTimelineIO-0.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:8a303b2f3dfba542f588b227575f1967f7a9da854b34f620504e1ecb8d551f5f"}, - {file = "OpenTimelineIO-0.14.1.tar.gz", hash = "sha256:0b9adc0fd303b978af120259d6b1d23e0623800615b4a3e2eb9f9fb2c70d5d13"}, -] - -[package.dependencies] -pyaaf2 = ">=1.4.0,<1.5.0" - -[package.extras] -dev = ["check-manifest", "coverage (>=4.5)", "flake8 (>=3.5)", "urllib3 (>=1.24.3)"] -view = ["PySide2 (>=5.11,<6.0)"] - [[package]] name = "packaging" version = "23.1" @@ -2224,17 +2184,6 @@ files = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -[[package]] -name = "pyaaf2" -version = "1.4.0" -description = "A python module for reading and writing advanced authoring format files" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "pyaaf2-1.4.0.tar.gz", hash = "sha256:160d3c26c7cfef7176d0bdb0e55772156570435982c3abfa415e89639f76e71b"}, -] - [[package]] name = "pyasn1" version = "0.5.0" diff --git a/pyproject.toml b/pyproject.toml index 1350b6e190..1da0880a67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ python = ">=3.9.1,<3.10" aiohttp = "^3.7" aiohttp_json_rpc = "*" # TVPaint server acre = { git = "https://github.com/pypeclub/acre.git" } -opentimelineio = "^0.14" appdirs = { git = "https://github.com/ActiveState/appdirs.git", branch = "master" } blessed = "^1.17" # openpype terminal formatting coolname = "*" @@ -131,10 +130,11 @@ version = "6.4.3" package = "PySide2" version = "5.15.2" -# DCC packages supply their own opencolorio, lets not interfere with theirs -[openpype.opencolorio] -package = "opencolorio" -version = "2.2.1" +# Python dependencies that will be available only in runtime of +# OpenPype process - do not interfere with DCCs dependencies +[openpype.runtime-deps] +opencolorio = "2.2.1" +opentimelineio = "0.14.1" # TODO: we will need to handle different linux flavours here and # also different macos versions too. diff --git a/tools/fetch_thirdparty_libs.py b/tools/fetch_thirdparty_libs.py index f06a74b292..c2dc4636d0 100644 --- a/tools/fetch_thirdparty_libs.py +++ b/tools/fetch_thirdparty_libs.py @@ -104,6 +104,8 @@ def install_qtbinding(pyproject, openpype_root, platform_name): version = qtbinding_def.get("version") _pip_install(openpype_root, package, version) + python_vendor_dir = openpype_root / "vendor" / "python" + # Remove libraries for QtSql which don't have available libraries # by default and Postgre library would require to modify rpath of # dependency @@ -115,12 +117,11 @@ def install_qtbinding(pyproject, openpype_root, platform_name): os.remove(str(filepath)) -def install_opencolorio(pyproject, openpype_root): - _print("Installing PyOpenColorIO") - opencolorio_def = pyproject["openpype"]["opencolorio"] - package = opencolorio_def["package"] - version = opencolorio_def.get("version") - _pip_install(openpype_root, package, version) +def install_runtime_dependencies(pyproject, openpype_root): + _print("Installing Runtime Dependencies ...") + runtime_deps = pyproject["openpype"]["runtime-deps"] + for package, version in runtime_deps.items(): + _pip_install(openpype_root, package, version) def install_thirdparty(pyproject, openpype_root, platform_name): @@ -232,7 +233,7 @@ def main(): pyproject = toml.load(openpype_root / "pyproject.toml") platform_name = platform.system().lower() install_qtbinding(pyproject, openpype_root, platform_name) - install_opencolorio(pyproject, openpype_root) + install_runtime_dependencies(pyproject, openpype_root) install_thirdparty(pyproject, openpype_root, platform_name) end_time = time.time_ns() total_time = (end_time - start_time) / 1000000000 From 2791aa84bbe2222ac0ffc157afb4b111f5d4718f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Jul 2023 17:14:22 +0200 Subject: [PATCH 255/446] AYON: Bundle distribution (#5209) * modified distribution to use bundles * use bundles in modules discovery logic * removed unused import * added support for bundle settings getter * added script launch mechanism to ayon start script * show login UI through subprocess * removed silent mode * removed unused variable * match env variables to ayon launcher * moved ui lib function to ayon common * raise custom exception on missing bundle name * implemented missing bundle window to show issues with bundles * implemented helper function to show dialog about issues with bundle * handle issues with bundles * removed unused import * dont convert passed addons infor * access keys only in server getters * fix accessed attribute * fix test * fixed missing 'message' variable * removed duplicated data * removed unnecessary 'sha256' variable * use lstrip instead of replacement * use f-string * move import to the top of file * added some dosctrings * change type * use f-string * fix grammar * set default settings variant in global connection creation * reuse new function * added init file * safe access to optional keys * removed unnecessary condition * modified print messages on issues with bundles * Changed message in missing bundle window * updated ayon_api to 0.3.2 --- ayon_start.py | 139 +++- common/ayon_common/__init__.py | 16 + common/ayon_common/connection/credentials.py | 49 +- common/ayon_common/connection/ui/__main__.py | 23 + .../ayon_common/connection/ui/login_window.py | 25 +- common/ayon_common/distribution/README.md | 4 +- common/ayon_common/distribution/__init__.py | 9 + common/ayon_common/distribution/addon_info.py | 213 ----- .../{addon_distribution.py => control.py} | 752 ++++++++---------- .../distribution/data_structures.py | 261 ++++++ .../ayon_common/distribution/downloaders.py | 250 ++++++ .../ayon_common/distribution/file_handler.py | 129 ++- .../tests/test_addon_distributtion.py | 253 +++--- .../distribution/ui/missing_bundle_window.py | 146 ++++ common/ayon_common/distribution/utils.py | 90 +++ common/ayon_common/resources/__init__.py | 4 +- common/ayon_common/resources/stylesheet.css | 2 +- .../{connection/ui/lib.py => ui_utils.py} | 0 common/ayon_common/utils.py | 40 +- openpype/modules/base.py | 39 +- openpype/settings/ayon_settings.py | 115 ++- .../vendor/python/common/ayon_api/__init__.py | 5 + .../python/common/ayon_api/server_api.py | 72 +- .../vendor/python/common/ayon_api/utils.py | 20 + .../vendor/python/common/ayon_api/version.py | 2 +- 25 files changed, 1741 insertions(+), 917 deletions(-) create mode 100644 common/ayon_common/__init__.py create mode 100644 common/ayon_common/connection/ui/__main__.py delete mode 100644 common/ayon_common/distribution/addon_info.py rename common/ayon_common/distribution/{addon_distribution.py => control.py} (63%) create mode 100644 common/ayon_common/distribution/data_structures.py create mode 100644 common/ayon_common/distribution/downloaders.py create mode 100644 common/ayon_common/distribution/ui/missing_bundle_window.py create mode 100644 common/ayon_common/distribution/utils.py rename common/ayon_common/{connection/ui/lib.py => ui_utils.py} (100%) diff --git a/ayon_start.py b/ayon_start.py index e45fbf4680..458c46bba6 100644 --- a/ayon_start.py +++ b/ayon_start.py @@ -9,6 +9,7 @@ import site import traceback import contextlib + # Enabled logging debug mode when "--debug" is passed if "--verbose" in sys.argv: expected_values = ( @@ -48,35 +49,55 @@ if "--verbose" in sys.argv: )) os.environ["OPENPYPE_LOG_LEVEL"] = str(log_level) + os.environ["AYON_LOG_LEVEL"] = str(log_level) # Enable debug mode, may affect log level if log level is not defined if "--debug" in sys.argv: sys.argv.remove("--debug") + os.environ["AYON_DEBUG"] = "1" os.environ["OPENPYPE_DEBUG"] = "1" if "--automatic-tests" in sys.argv: sys.argv.remove("--automatic-tests") os.environ["IS_TEST"] = "1" +SKIP_HEADERS = False +if "--skip-headers" in sys.argv: + sys.argv.remove("--skip-headers") + SKIP_HEADERS = True + +SKIP_BOOTSTRAP = False +if "--skip-bootstrap" in sys.argv: + sys.argv.remove("--skip-bootstrap") + SKIP_BOOTSTRAP = True + if "--use-staging" in sys.argv: sys.argv.remove("--use-staging") + os.environ["AYON_USE_STAGING"] = "1" os.environ["OPENPYPE_USE_STAGING"] = "1" -_silent_commands = { - "run", - "standalonepublisher", - "extractenvironments", - "version" -} if "--headless" in sys.argv: + os.environ["AYON_HEADLESS_MODE"] = "1" os.environ["OPENPYPE_HEADLESS_MODE"] = "1" sys.argv.remove("--headless") -elif os.getenv("OPENPYPE_HEADLESS_MODE") != "1": + +elif ( + os.getenv("AYON_HEADLESS_MODE") != "1" + or os.getenv("OPENPYPE_HEADLESS_MODE") != "1" +): + os.environ.pop("AYON_HEADLESS_MODE", None) os.environ.pop("OPENPYPE_HEADLESS_MODE", None) +elif ( + os.getenv("AYON_HEADLESS_MODE") + != os.getenv("OPENPYPE_HEADLESS_MODE") +): + os.environ["OPENPYPE_HEADLESS_MODE"] = ( + os.environ["AYON_HEADLESS_MODE"] + ) + IS_BUILT_APPLICATION = getattr(sys, "frozen", False) -HEADLESS_MODE_ENABLED = os.environ.get("OPENPYPE_HEADLESS_MODE") == "1" -SILENT_MODE_ENABLED = any(arg in _silent_commands for arg in sys.argv) +HEADLESS_MODE_ENABLED = os.getenv("AYON_HEADLESS_MODE") == "1" _pythonpath = os.getenv("PYTHONPATH", "") _python_paths = _pythonpath.split(os.pathsep) @@ -129,10 +150,12 @@ os.environ["PYTHONPATH"] = os.pathsep.join(_python_paths) os.environ["USE_AYON_SERVER"] = "1" # Set this to point either to `python` from venv in case of live code # or to `ayon` or `ayon_console` in case of frozen code +os.environ["AYON_EXECUTABLE"] = sys.executable os.environ["OPENPYPE_EXECUTABLE"] = sys.executable os.environ["AYON_ROOT"] = AYON_ROOT os.environ["OPENPYPE_ROOT"] = AYON_ROOT os.environ["OPENPYPE_REPOS_ROOT"] = AYON_ROOT +os.environ["AYON_MENU_LABEL"] = "AYON" os.environ["AVALON_LABEL"] = "AYON" # Set name of pyblish UI import os.environ["PYBLISH_GUI"] = "pyblish_pype" @@ -153,9 +176,7 @@ if sys.__stdout__: term = blessed.Terminal() def _print(message: str): - if SILENT_MODE_ENABLED: - pass - elif message.startswith("!!! "): + if message.startswith("!!! "): print(f'{term.orangered2("!!! ")}{message[4:]}') elif message.startswith(">>> "): print(f'{term.aquamarine3(">>> ")}{message[4:]}') @@ -179,8 +200,7 @@ if sys.__stdout__: print(message) else: def _print(message: str): - if not SILENT_MODE_ENABLED: - print(message) + print(message) # if SSL_CERT_FILE is not set prior to OpenPype launch, we set it to point @@ -190,7 +210,9 @@ if not os.getenv("SSL_CERT_FILE"): elif os.getenv("SSL_CERT_FILE") != certifi.where(): _print("--- your system is set to use custom CA certificate bundle.") +from ayon_api import get_base_url from ayon_api.constants import SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY +from ayon_common import is_staging_enabled from ayon_common.connection.credentials import ( ask_to_login_ui, add_server, @@ -200,7 +222,11 @@ from ayon_common.connection.credentials import ( create_global_connection, confirm_server_login, ) -from ayon_common.distribution.addon_distribution import AyonDistribution +from ayon_common.distribution import ( + AyonDistribution, + BundleNotFoundError, + show_missing_bundle_information, +) def set_global_environments() -> None: @@ -286,8 +312,44 @@ def _check_and_update_from_ayon_server(): """ distribution = AyonDistribution() + bundle = None + bundle_name = None + try: + bundle = distribution.bundle_to_use + if bundle is not None: + bundle_name = bundle.name + except BundleNotFoundError as exc: + bundle_name = exc.bundle_name + + if bundle is None: + url = get_base_url() + if not HEADLESS_MODE_ENABLED: + show_missing_bundle_information(url, bundle_name) + + elif bundle_name: + _print(( + f"!!! Requested release bundle '{bundle_name}'" + " is not available on server." + )) + _print( + "!!! Check if selected release bundle" + f" is available on the server '{url}'." + ) + + else: + mode = "staging" if is_staging_enabled() else "production" + _print( + f"!!! No release bundle is set as {mode} on the AYON server." + ) + _print( + "!!! Make sure there is a release bundle set" + f" as \"{mode}\" on the AYON server '{url}'." + ) + sys.exit(1) + distribution.distribute() distribution.validate_distribution() + os.environ["AYON_BUNDLE_NAME"] = bundle_name python_paths = [ path @@ -311,8 +373,6 @@ def boot(): os.environ["OPENPYPE_VERSION"] = __version__ os.environ["AYON_VERSION"] = __version__ - use_staging = os.environ.get("OPENPYPE_USE_STAGING") == "1" - _connect_to_ayon_server() _check_and_update_from_ayon_server() @@ -328,7 +388,10 @@ def boot(): with contextlib.suppress(AttributeError, KeyError): del sys.modules[module_name] + +def main_cli(): from openpype import cli + from openpype.version import __version__ from openpype.lib import terminal as t _print(">>> loading environments ...") @@ -338,8 +401,8 @@ def boot(): set_addons_environments() # print info when not running scripts defined in 'silent commands' - if not SILENT_MODE_ENABLED: - info = get_info(use_staging) + if not SKIP_HEADERS: + info = get_info(is_staging_enabled()) info.insert(0, f">>> Using AYON from [ {AYON_ROOT} ]") t_width = 20 @@ -361,6 +424,29 @@ def boot(): sys.exit(1) +def script_cli(): + """Run and execute script.""" + + filepath = os.path.abspath(sys.argv[1]) + + # Find '__main__.py' in directory + if os.path.isdir(filepath): + new_filepath = os.path.join(filepath, "__main__.py") + if not os.path.exists(new_filepath): + raise RuntimeError( + f"can't find '__main__' module in '{filepath}'") + filepath = new_filepath + + # Add parent dir to sys path + sys.path.insert(0, os.path.dirname(filepath)) + + # Read content and execute + with open(filepath, "r") as stream: + content = stream.read() + + exec(compile(content, filepath, "exec"), globals()) + + def get_info(use_staging=None) -> list: """Print additional information to console.""" @@ -369,6 +455,7 @@ def get_info(use_staging=None) -> list: inf.append(("AYON variant", "staging")) else: inf.append(("AYON variant", "production")) + inf.append(("AYON bundle", os.getenv("AYON_BUNDLE"))) # NOTE add addons information @@ -380,5 +467,17 @@ def get_info(use_staging=None) -> list: return formatted +def main(): + if not SKIP_BOOTSTRAP: + boot() + + args = list(sys.argv) + args.pop(0) + if args and os.path.exists(args[0]): + script_cli() + else: + main_cli() + + if __name__ == "__main__": - boot() + main() diff --git a/common/ayon_common/__init__.py b/common/ayon_common/__init__.py new file mode 100644 index 0000000000..ddabb7da2f --- /dev/null +++ b/common/ayon_common/__init__.py @@ -0,0 +1,16 @@ +from .utils import ( + IS_BUILT_APPLICATION, + is_staging_enabled, + get_local_site_id, + get_ayon_appdirs, + get_ayon_launch_args, +) + + +__all__ = ( + "IS_BUILT_APPLICATION", + "is_staging_enabled", + "get_local_site_id", + "get_ayon_appdirs", + "get_ayon_launch_args", +) diff --git a/common/ayon_common/connection/credentials.py b/common/ayon_common/connection/credentials.py index 23cac9a8fc..ad2ca9a6b2 100644 --- a/common/ayon_common/connection/credentials.py +++ b/common/ayon_common/connection/credentials.py @@ -13,6 +13,8 @@ import json import platform import datetime import contextlib +import subprocess +import tempfile from typing import Optional, Union, Any import ayon_api @@ -25,7 +27,12 @@ from ayon_api.utils import ( logout_from_server, ) -from ayon_common.utils import get_ayon_appdirs, get_local_site_id +from ayon_common.utils import ( + get_ayon_appdirs, + get_local_site_id, + get_ayon_launch_args, + is_staging_enabled, +) class ChangeUserResult: @@ -258,6 +265,8 @@ def ask_to_login_ui( credentials are invalid. To change credentials use 'change_user_ui' function. + Use a subprocess to show UI. + Args: url (Optional[str]): Server url that could be prefilled in UI. always_on_top (Optional[bool]): Window will be drawn on top of @@ -267,12 +276,33 @@ def ask_to_login_ui( tuple[str, str, str]: Url, user's token and username. """ - from .ui import ask_to_login + current_dir = os.path.dirname(os.path.abspath(__file__)) + ui_dir = os.path.join(current_dir, "ui") if url is None: url = get_last_server() username = get_last_username_by_url(url) - return ask_to_login(url, username, always_on_top=always_on_top) + data = { + "url": url, + "username": username, + "always_on_top": always_on_top, + } + + with tempfile.TemporaryFile( + mode="w", prefix="ayon_login", suffix=".json", delete=False + ) as tmp: + output = tmp.name + json.dump(data, tmp) + + code = subprocess.call( + get_ayon_launch_args(ui_dir, "--skip-bootstrap", output)) + if code != 0: + raise RuntimeError("Failed to show login UI") + + with open(output, "r") as stream: + data = json.load(stream) + os.remove(output) + return data["output"] def change_user_ui() -> ChangeUserResult: @@ -421,15 +451,18 @@ def set_environments(url: str, token: str): def create_global_connection(): """Create global connection with site id and client version. - Make sure the global connection in 'ayon_api' have entered site id and client version. + + Set default settings variant to use based on 'is_staging_enabled'. """ - if hasattr(ayon_api, "create_connection"): - ayon_api.create_connection( - get_local_site_id(), os.environ.get("AYON_VERSION") - ) + ayon_api.create_connection( + get_local_site_id(), os.environ.get("AYON_VERSION") + ) + ayon_api.set_default_settings_variant( + "staging" if is_staging_enabled() else "production" + ) def need_server_or_login() -> bool: diff --git a/common/ayon_common/connection/ui/__main__.py b/common/ayon_common/connection/ui/__main__.py new file mode 100644 index 0000000000..719b2b8ef5 --- /dev/null +++ b/common/ayon_common/connection/ui/__main__.py @@ -0,0 +1,23 @@ +import sys +import json + +from ayon_common.connection.ui.login_window import ask_to_login + + +def main(output_path): + with open(output_path, "r") as stream: + data = json.load(stream) + + url = data.get("url") + username = data.get("username") + always_on_top = data.get("always_on_top", False) + out_url, out_token, out_username = ask_to_login( + url, username, always_on_top=always_on_top) + + data["output"] = [out_url, out_token, out_username] + with open(output_path, "w") as stream: + json.dump(data, stream) + + +if __name__ == "__main__": + main(sys.argv[-1]) diff --git a/common/ayon_common/connection/ui/login_window.py b/common/ayon_common/connection/ui/login_window.py index 566dc4f71f..94c239852e 100644 --- a/common/ayon_common/connection/ui/login_window.py +++ b/common/ayon_common/connection/ui/login_window.py @@ -10,12 +10,12 @@ from ayon_common.resources import ( get_icon_path, load_stylesheet, ) +from ayon_common.ui_utils import set_style_property, get_qt_app from .widgets import ( PressHoverButton, PlaceholderLineEdit, ) -from .lib import set_style_property class LogoutConfirmDialog(QtWidgets.QDialog): @@ -650,16 +650,7 @@ def ask_to_login(url=None, username=None, always_on_top=False): be changed during dialog lifetime that's why the url is returned. """ - app_instance = QtWidgets.QApplication.instance() - if app_instance is None: - for attr_name in ( - "AA_EnableHighDpiScaling", - "AA_UseHighDpiPixmaps" - ): - attr = getattr(QtCore.Qt, attr_name, None) - if attr is not None: - QtWidgets.QApplication.setAttribute(attr) - app_instance = QtWidgets.QApplication([]) + app_instance = get_qt_app() window = ServerLoginWindow() if always_on_top: @@ -701,17 +692,7 @@ def change_user(url, username, api_key, always_on_top=False): during dialog lifetime that's why the url is returned. """ - app_instance = QtWidgets.QApplication.instance() - if app_instance is None: - for attr_name in ( - "AA_EnableHighDpiScaling", - "AA_UseHighDpiPixmaps" - ): - attr = getattr(QtCore.Qt, attr_name, None) - if attr is not None: - QtWidgets.QApplication.setAttribute(attr) - app_instance = QtWidgets.QApplication([]) - + app_instance = get_qt_app() window = ServerLoginWindow() if always_on_top: window.setWindowFlags( diff --git a/common/ayon_common/distribution/README.md b/common/ayon_common/distribution/README.md index 212eb267b8..f1c34ba722 100644 --- a/common/ayon_common/distribution/README.md +++ b/common/ayon_common/distribution/README.md @@ -5,7 +5,7 @@ Code in this folder is backend portion of Addon distribution logic for v4 server Each host, module will be separate Addon in the future. Each v4 server could run different set of Addons. -Client (running on artist machine) will in the first step ask v4 for list of enabled addons. +Client (running on artist machine) will in the first step ask v4 for list of enabled addons. (It expects list of json documents matching to `addon_distribution.py:AddonInfo` object.) Next it will compare presence of enabled addon version in local folder. In the case of missing version of an addon, client will use information in the addon to download (from http/shared local disk/git) zip file @@ -15,4 +15,4 @@ Required part of addon distribution will be sharing of dependencies (python libr Location of this folder might change in the future as it will be required for a clint to add this folder to sys.path reliably. -This code needs to be independent on Openpype code as much as possible! \ No newline at end of file +This code needs to be independent on Openpype code as much as possible! diff --git a/common/ayon_common/distribution/__init__.py b/common/ayon_common/distribution/__init__.py index e69de29bb2..e3c0f0e161 100644 --- a/common/ayon_common/distribution/__init__.py +++ b/common/ayon_common/distribution/__init__.py @@ -0,0 +1,9 @@ +from .control import AyonDistribution, BundleNotFoundError +from .utils import show_missing_bundle_information + + +__all__ = ( + "AyonDistribution", + "BundleNotFoundError", + "show_missing_bundle_information", +) diff --git a/common/ayon_common/distribution/addon_info.py b/common/ayon_common/distribution/addon_info.py deleted file mode 100644 index 74f7b11f7f..0000000000 --- a/common/ayon_common/distribution/addon_info.py +++ /dev/null @@ -1,213 +0,0 @@ -import attr -from enum import Enum - - -class UrlType(Enum): - HTTP = "http" - GIT = "git" - FILESYSTEM = "filesystem" - SERVER = "server" - - -@attr.s -class MultiPlatformPath(object): - windows = attr.ib(default=None) - linux = attr.ib(default=None) - darwin = attr.ib(default=None) - - -@attr.s -class SourceInfo(object): - type = attr.ib() - - -@attr.s -class LocalSourceInfo(SourceInfo): - path = attr.ib(default=attr.Factory(MultiPlatformPath)) - - -@attr.s -class WebSourceInfo(SourceInfo): - url = attr.ib(default=None) - headers = attr.ib(default=None) - filename = attr.ib(default=None) - - -@attr.s -class ServerSourceInfo(SourceInfo): - filename = attr.ib(default=None) - path = attr.ib(default=None) - - -def convert_source(source): - """Create source object from data information. - - Args: - source (Dict[str, any]): Information about source. - - Returns: - Union[None, SourceInfo]: Object with source information if type is - known. - """ - - source_type = source.get("type") - if not source_type: - return None - - if source_type == UrlType.FILESYSTEM.value: - return LocalSourceInfo( - type=source_type, - path=source["path"] - ) - - if source_type == UrlType.HTTP.value: - url = source["path"] - return WebSourceInfo( - type=source_type, - url=url, - headers=source.get("headers"), - filename=source.get("filename") - ) - - if source_type == UrlType.SERVER.value: - return ServerSourceInfo( - type=source_type, - filename=source.get("filename"), - path=source.get("path") - ) - - -@attr.s -class VersionData(object): - version_data = attr.ib(default=None) - - -@attr.s -class AddonInfo(object): - """Object matching json payload from Server""" - name = attr.ib() - version = attr.ib() - full_name = attr.ib() - title = attr.ib(default=None) - require_distribution = attr.ib(default=False) - sources = attr.ib(default=attr.Factory(list)) - unknown_sources = attr.ib(default=attr.Factory(list)) - hash = attr.ib(default=None) - description = attr.ib(default=None) - license = attr.ib(default=None) - authors = attr.ib(default=None) - - @classmethod - def from_dict_by_version(cls, data, addon_version): - """Addon info for specific version. - - Args: - data (dict[str, Any]): Addon information from server. Should - contain information about every version under 'versions'. - addon_version (str): Addon version for which is info requested. - - Returns: - Union[AddonInfo, None]: Addon info, or None if version is not - available. - """ - - if not addon_version: - return None - - # server payload contains info about all versions - version_data = data.get("versions", {}).get(addon_version) - if not version_data: - return None - - source_info = version_data.get("clientSourceInfo") - require_distribution = source_info is not None - - sources = [] - unknown_sources = [] - for source in (source_info or []): - addon_source = convert_source(source) - if addon_source is not None: - sources.append(addon_source) - else: - unknown_sources.append(source) - print(f"Unknown source {source.get('type')}") - - full_name = "{}_{}".format(data["name"], addon_version) - return cls( - name=data.get("name"), - version=addon_version, - full_name=full_name, - require_distribution=require_distribution, - sources=sources, - unknown_sources=unknown_sources, - hash=data.get("hash"), - description=data.get("description"), - title=data.get("title"), - license=data.get("license"), - authors=data.get("authors") - ) - - @classmethod - def from_dict(cls, data, use_staging=False): - """Get Addon information for production or staging version. - - Args: - data (dict[str, Any]): Addon information from server. Should - contain information about every version under 'versions'. - use_staging (bool): Use staging version if set to 'True' instead - of production. - - Returns: - Union[AddonInfo, None]: Addon info, or None if version is not - set or available. - """ - - # Active addon must have 'productionVersion' or 'stagingVersion' - # and matching version info. - if use_staging: - addon_version = data.get("stagingVersion") - else: - addon_version = data.get("productionVersion") - return cls.from_dict_by_version(data, addon_version) - - -@attr.s -class DependencyItem(object): - """Object matching payload from Server about single dependency package""" - name = attr.ib() - platform = attr.ib() - checksum = attr.ib() - require_distribution = attr.ib() - sources = attr.ib(default=attr.Factory(list)) - unknown_sources = attr.ib(default=attr.Factory(list)) - addon_list = attr.ib(default=attr.Factory(list)) - python_modules = attr.ib(default=attr.Factory(dict)) - - @classmethod - def from_dict(cls, package): - sources = [] - unknown_sources = [] - package_sources = package.get("sources") - require_distribution = package_sources is not None - for source in (package_sources or []): - dependency_source = convert_source(source) - if dependency_source is not None: - sources.append(dependency_source) - else: - print(f"Unknown source {source.get('type')}") - unknown_sources.append(source) - - addon_list = [f"{name}_{version}" - for name, version in - package.get("supportedAddons").items()] - - return cls( - name=package.get("name"), - platform=package.get("platform"), - require_distribution=require_distribution, - sources=sources, - unknown_sources=unknown_sources, - checksum=package.get("checksum"), - addon_list=addon_list, - python_modules=package.get("pythonModules") - ) diff --git a/common/ayon_common/distribution/addon_distribution.py b/common/ayon_common/distribution/control.py similarity index 63% rename from common/ayon_common/distribution/addon_distribution.py rename to common/ayon_common/distribution/control.py index 19aec2b031..7b38a9a9af 100644 --- a/common/ayon_common/distribution/addon_distribution.py +++ b/common/ayon_common/distribution/control.py @@ -4,24 +4,43 @@ import json import traceback import collections import datetime -from enum import Enum -from abc import abstractmethod -import attr import logging -import platform import shutil import threading -from abc import ABCMeta +import platform +import attr +from enum import Enum import ayon_api -from ayon_common.utils import get_ayon_appdirs -from .file_handler import RemoteFileHandler -from .addon_info import ( - AddonInfo, - UrlType, - DependencyItem, +from ayon_common.utils import is_staging_enabled + +from .utils import ( + get_addons_dir, + get_dependencies_dir, ) +from .downloaders import get_default_download_factory +from .data_structures import ( + AddonInfo, + DependencyItem, + Bundle, +) + +NOT_SET = type("UNKNOWN", (), {"__bool__": lambda: False})() + + +class BundleNotFoundError(Exception): + """Bundle name is defined but is not available on server. + + Args: + bundle_name (str): Name of bundle that was not found. + """ + + def __init__(self, bundle_name): + self.bundle_name = bundle_name + super().__init__( + f"Bundle '{bundle_name}' is not available on server" + ) class UpdateState(Enum): @@ -32,326 +51,6 @@ class UpdateState(Enum): MISS_SOURCE_FILES = "miss_source_files" -def get_local_dir(*subdirs): - """Get product directory in user's home directory. - - Each user on machine have own local directory where are downloaded updates, - addons etc. - - Returns: - str: Path to product local directory. - """ - - if not subdirs: - raise ValueError("Must fill dir_name if nothing else provided!") - - local_dir = get_ayon_appdirs(*subdirs) - if not os.path.isdir(local_dir): - try: - os.makedirs(local_dir) - except Exception: # TODO fix exception - raise RuntimeError(f"Cannot create {local_dir}") - - return local_dir - - -def get_addons_dir(): - """Directory where addon packages are stored. - - Path to addons is defined using python module 'appdirs' which - - The path is stored into environment variable 'AYON_ADDONS_DIR'. - Value of environment variable can be overriden, but we highly recommended - to use that option only for development purposes. - - Returns: - str: Path to directory where addons should be downloaded. - """ - - addons_dir = os.environ.get("AYON_ADDONS_DIR") - if not addons_dir: - addons_dir = get_local_dir("addons") - os.environ["AYON_ADDONS_DIR"] = addons_dir - return addons_dir - - -def get_dependencies_dir(): - """Directory where dependency packages are stored. - - Path to addons is defined using python module 'appdirs' which - - The path is stored into environment variable 'AYON_DEPENDENCIES_DIR'. - Value of environment variable can be overriden, but we highly recommended - to use that option only for development purposes. - - Returns: - str: Path to directory where dependency packages should be downloaded. - """ - - dependencies_dir = os.environ.get("AYON_DEPENDENCIES_DIR") - if not dependencies_dir: - dependencies_dir = get_local_dir("dependency_packages") - os.environ["AYON_DEPENDENCIES_DIR"] = dependencies_dir - return dependencies_dir - - -class SourceDownloader(metaclass=ABCMeta): - log = logging.getLogger(__name__) - - @classmethod - @abstractmethod - def download(cls, source, destination_dir, data, transfer_progress): - """Returns url to downloaded addon zip file. - - Tranfer progress can be ignored, in that case file transfer won't - be shown as 0-100% but as 'running'. First step should be to set - destination content size and then add transferred chunk sizes. - - Args: - source (dict): {type:"http", "url":"https://} ...} - destination_dir (str): local folder to unzip - data (dict): More information about download content. Always have - 'type' key in. - transfer_progress (ayon_api.TransferProgress): Progress of - transferred (copy/download) content. - - Returns: - (str) local path to addon zip file - """ - - pass - - @classmethod - @abstractmethod - def cleanup(cls, source, destination_dir, data): - """Cleanup files when distribution finishes or crashes. - - Cleanup e.g. temporary files (downloaded zip) or other related stuff - to downloader. - """ - - pass - - @classmethod - def check_hash(cls, addon_path, addon_hash, hash_type="sha256"): - """Compares 'hash' of downloaded 'addon_url' file. - - Args: - addon_path (str): Local path to addon file. - addon_hash (str): Hash of downloaded file. - hash_type (str): Type of hash. - - Raises: - ValueError if hashes doesn't match - """ - - if not os.path.exists(addon_path): - raise ValueError(f"{addon_path} doesn't exist.") - if not RemoteFileHandler.check_integrity(addon_path, - addon_hash, - hash_type=hash_type): - raise ValueError(f"{addon_path} doesn't match expected hash.") - - @classmethod - def unzip(cls, addon_zip_path, destination_dir): - """Unzips local 'addon_zip_path' to 'destination'. - - Args: - addon_zip_path (str): local path to addon zip file - destination_dir (str): local folder to unzip - """ - - RemoteFileHandler.unzip(addon_zip_path, destination_dir) - os.remove(addon_zip_path) - - -class DownloadFactory: - def __init__(self): - self._downloaders = {} - - def register_format(self, downloader_type, downloader): - """Register downloader for download type. - - Args: - downloader_type (UrlType): Type of source. - downloader (SourceDownloader): Downloader which cares about - download, hash check and unzipping. - """ - - self._downloaders[downloader_type.value] = downloader - - def get_downloader(self, downloader_type): - """Registered downloader for type. - - Args: - downloader_type (UrlType): Type of source. - - Returns: - SourceDownloader: Downloader object which should care about file - distribution. - - Raises: - ValueError: If type does not have registered downloader. - """ - - if downloader := self._downloaders.get(downloader_type): - return downloader() - raise ValueError(f"{downloader_type} not implemented") - - -class OSDownloader(SourceDownloader): - @classmethod - def download(cls, source, destination_dir, data, transfer_progress): - # OS doesn't need to download, unzip directly - addon_url = source["path"].get(platform.system().lower()) - if not os.path.exists(addon_url): - raise ValueError(f"{addon_url} is not accessible") - return addon_url - - @classmethod - def cleanup(cls, source, destination_dir, data): - # Nothing to do - download does not copy anything - pass - - -class HTTPDownloader(SourceDownloader): - CHUNK_SIZE = 100000 - - @staticmethod - def get_filename(source): - source_url = source["url"] - filename = source.get("filename") - if not filename: - filename = os.path.basename(source_url) - basename, ext = os.path.splitext(filename) - allowed_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS) - if ext.replace(".", "") not in allowed_exts: - filename = f"{basename}.zip" - return filename - - @classmethod - def download(cls, source, destination_dir, data, transfer_progress): - source_url = source["url"] - cls.log.debug(f"Downloading {source_url} to {destination_dir}") - headers = source.get("headers") - filename = cls.get_filename(source) - - # TODO use transfer progress - RemoteFileHandler.download_url( - source_url, - destination_dir, - filename, - headers=headers - ) - - return os.path.join(destination_dir, filename) - - @classmethod - def cleanup(cls, source, destination_dir, data): - # Nothing to do - download does not copy anything - filename = cls.get_filename(source) - filepath = os.path.join(destination_dir, filename) - if os.path.exists(filepath) and os.path.isfile(filepath): - os.remove(filepath) - - -class AyonServerDownloader(SourceDownloader): - """Downloads static resource file from v4 Server. - - Expects filled env var AYON_SERVER_URL. - """ - - CHUNK_SIZE = 8192 - - @classmethod - def download(cls, source, destination_dir, data, transfer_progress): - path = source["path"] - filename = source["filename"] - if path and not filename: - filename = path.split("/")[-1] - - cls.log.debug(f"Downloading {filename} to {destination_dir}") - - _, ext = os.path.splitext(filename) - clear_ext = ext.lower().replace(".", "") - valid_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS) - if clear_ext not in valid_exts: - raise ValueError( - "Invalid file extension \"{}\". Expected {}".format( - clear_ext, ", ".join(valid_exts) - )) - - if path: - filepath = os.path.join(destination_dir, filename) - return ayon_api.download_file( - path, - filepath, - chunk_size=cls.CHUNK_SIZE, - progress=transfer_progress - ) - - # dst_filepath = os.path.join(destination_dir, filename) - if data["type"] == "dependency_package": - return ayon_api.download_dependency_package( - data["name"], - destination_dir, - filename, - platform_name=data["platform"], - chunk_size=cls.CHUNK_SIZE, - progress=transfer_progress - ) - - if data["type"] == "addon": - return ayon_api.download_addon_private_file( - data["name"], - data["version"], - filename, - destination_dir, - chunk_size=cls.CHUNK_SIZE, - progress=transfer_progress - ) - - raise ValueError(f"Unknown type to download \"{data['type']}\"") - - @classmethod - def cleanup(cls, source, destination_dir, data): - # Nothing to do - download does not copy anything - filename = source["filename"] - filepath = os.path.join(destination_dir, filename) - if os.path.exists(filepath) and os.path.isfile(filepath): - os.remove(filepath) - - -def get_dependency_package(package_name=None): - """Returns info about currently used dependency package. - - Dependency package means .venv created from all activated addons from the - server (plus libraries for core Tray app TODO confirm). - This package needs to be downloaded, unpacked and added to sys.path for - Tray app to work. - - Args: - package_name (str): Name of package. Production package name is used - if not entered. - - Returns: - Union[DependencyItem, None]: Item or None if package with the name was - not found. - """ - - dependencies_info = ayon_api.get_dependencies_info() - - dependency_list = dependencies_info["packages"] - # Use production package if package is not specified - if package_name is None: - package_name = dependencies_info["productionPackage"] - - for dependency in dependency_list: - dependency_package = DependencyItem.from_dict(dependency) - if dependency_package.name == package_name: - return dependency_package - - class DistributeTransferProgress: """Progress of single source item in 'DistributionItem'. @@ -612,7 +311,8 @@ class DistributionItem: try: downloader = self.factory.get_downloader(source.type) except Exception: - source_progress.set_failed(f"Unknown downloader {source.type}") + message = f"Unknown downloader {source.type}" + source_progress.set_failed(message) self.log.warning(message, exc_info=True) continue @@ -733,12 +433,18 @@ class AyonDistribution: dependency_dirpath (Optional[str]): Where dependencies will be stored. dist_factory (Optional[DownloadFactory]): Factory which cares about downloading of items based on source type. - addons_info (Optional[List[AddonInfo]]): List of prepared addons' info. - dependency_package_info (Optional[Union[Dict[str, Any], None]]): Info - about package from server. Defaults to '-1'. + addons_info (Optional[list[dict[str, Any]]): List of prepared + addons' info. + dependency_packages_info (Optional[list[dict[str, Any]]): Info + about packages from server. + bundles_info (Optional[Dict[str, Any]]): Info about + bundles. + bundle_name (Optional[str]): Name of bundle to use. If not passed + an environment variable 'AYON_BUNDLE_NAME' is checked for value. + When both are not available the bundle is defined by 'use_staging' + value. use_staging (Optional[bool]): Use staging versions of an addon. - If not passed, an environment variable 'OPENPYPE_USE_STAGING' is - checked for value '1'. + If not passed, 'is_staging_enabled' is used as default value. """ def __init__( @@ -746,96 +452,304 @@ class AyonDistribution: addon_dirpath=None, dependency_dirpath=None, dist_factory=None, - addons_info=None, - dependency_package_info=-1, + addons_info=NOT_SET, + dependency_packages_info=NOT_SET, + bundles_info=NOT_SET, + bundle_name=NOT_SET, use_staging=None ): + self._log = None + + self._dist_started = False + self._dist_finished = False + self._addons_dirpath = addon_dirpath or get_addons_dir() self._dependency_dirpath = dependency_dirpath or get_dependencies_dir() self._dist_factory = ( dist_factory or get_default_download_factory() ) - if isinstance(addons_info, list): - addons_info = {item.full_name: item for item in addons_info} - self._dist_started = False - self._dist_finished = False - self._log = None + if bundle_name is NOT_SET: + bundle_name = os.environ.get("AYON_BUNDLE_NAME", NOT_SET) + + # Raw addons data from server self._addons_info = addons_info - self._addons_dist_items = None - self._dependency_package = dependency_package_info - self._dependency_dist_item = -1 + # Prepared data as Addon objects + self._addon_items = NOT_SET + # Distrubtion items of addons + # - only those addons and versions that should be distributed + self._addon_dist_items = NOT_SET + + # Raw dependency packages data from server + self._dependency_packages_info = dependency_packages_info + # Prepared dependency packages as objects + self._dependency_packages_items = NOT_SET + # Dependency package item that should be used + self._dependency_package_item = NOT_SET + # Distribution item of dependency package + self._dependency_dist_item = NOT_SET + + # Raw bundles data from server + self._bundles_info = bundles_info + # Bundles as objects + self._bundle_items = NOT_SET + + # Bundle that should be used in production + self._production_bundle = NOT_SET + # Bundle that should be used in staging + self._staging_bundle = NOT_SET + # Boolean that defines if staging bundle should be used self._use_staging = use_staging + # Specific bundle name should be used + self._bundle_name = bundle_name + # Final bundle that will be used + self._bundle = NOT_SET + @property def use_staging(self): + """Staging version of a bundle should be used. + + This value is completely ignored if specific bundle name should + be used. + + Returns: + bool: True if staging version should be used. + """ + if self._use_staging is None: - self._use_staging = os.getenv("OPENPYPE_USE_STAGING") == "1" + self._use_staging = is_staging_enabled() return self._use_staging @property def log(self): + """Helper to access logger. + + Returns: + logging.Logger: Logger instance. + """ if self._log is None: self._log = logging.getLogger(self.__class__.__name__) return self._log + @property + def bundles_info(self): + """ + + Returns: + dict[str, dict[str, Any]]: Bundles information from server. + """ + + if self._bundles_info is NOT_SET: + self._bundles_info = ayon_api.get_bundles() + return self._bundles_info + + @property + def bundle_items(self): + """ + + Returns: + list[Bundle]: List of bundles info. + """ + + if self._bundle_items is NOT_SET: + self._bundle_items = [ + Bundle.from_dict(info) + for info in self.bundles_info["bundles"] + ] + return self._bundle_items + + def _prepare_production_staging_bundles(self): + production_bundle = None + staging_bundle = None + for bundle in self.bundle_items: + if bundle.is_production: + production_bundle = bundle + if bundle.is_staging: + staging_bundle = bundle + self._production_bundle = production_bundle + self._staging_bundle = staging_bundle + + @property + def production_bundle(self): + """ + Returns: + Union[Bundle, None]: Bundle that should be used in production. + """ + + if self._production_bundle is NOT_SET: + self._prepare_production_staging_bundles() + return self._production_bundle + + @property + def staging_bundle(self): + """ + Returns: + Union[Bundle, None]: Bundle that should be used in staging. + """ + + if self._staging_bundle is NOT_SET: + self._prepare_production_staging_bundles() + return self._staging_bundle + + @property + def bundle_to_use(self): + """Bundle that will be used for distribution. + + Bundle that should be used can be affected by 'bundle_name' + or 'use_staging'. + + Returns: + Union[Bundle, None]: Bundle that will be used for distribution + or None. + + Raises: + BundleNotFoundError: When bundle name to use is defined + but is not available on server. + """ + + if self._bundle is NOT_SET: + if self._bundle_name is not NOT_SET: + bundle = next( + ( + bundle + for bundle in self.bundle_items + if bundle.name == self._bundle_name + ), + None + ) + if bundle is None: + raise BundleNotFoundError(self._bundle_name) + + self._bundle = bundle + elif self.use_staging: + self._bundle = self.staging_bundle + else: + self._bundle = self.production_bundle + return self._bundle + + @property + def bundle_name_to_use(self): + bundle = self.bundle_to_use + return None if bundle is None else bundle.name + @property def addons_info(self): + """Server information about available addons. + + Returns: + Dict[str, dict[str, Any]: Addon info by addon name. + """ + + if self._addons_info is NOT_SET: + server_info = ayon_api.get_addons_info(details=True) + self._addons_info = server_info["addons"] + return self._addons_info + + @property + def addon_items(self): """Information about available addons on server. Addons may require distribution of files. For those addons will be created 'DistributionItem' handling distribution itself. - Todos: - Add support for staging versions. Right now is supported only - production version. - Returns: - Dict[str, AddonInfo]: Addon info by full name. + Dict[str, AddonInfo]: Addon info object by addon name. """ - if self._addons_info is None: + if self._addon_items is NOT_SET: addons_info = {} - server_addons_info = ayon_api.get_addons_info(details=True) - for addon in server_addons_info["addons"]: - addon_info = AddonInfo.from_dict(addon, self.use_staging) - if addon_info is None: - continue - addons_info[addon_info.full_name] = addon_info - - self._addons_info = addons_info - return self._addons_info + for addon in self.addons_info: + addon_info = AddonInfo.from_dict(addon) + addons_info[addon_info.name] = addon_info + self._addon_items = addons_info + return self._addon_items @property - def dependency_package(self): - """Information about dependency package from server. - - Receive and cache dependency package information from server. + def dependency_packages_info(self): + """Server information about available dependency packages. Notes: - For testing purposes it is possible to pass dependency package + For testing purposes it is possible to pass dependency packages information to '__init__'. Returns: - Union[None, Dict[str, Any]]: None if server does not have specified - dependency package. + list[dict[str, Any]]: Dependency packages information. """ - if self._dependency_package == -1: - self._dependency_package = get_dependency_package() - return self._dependency_package + if self._dependency_packages_info is NOT_SET: + self._dependency_packages_info = ( + ayon_api.get_dependency_packages())["packages"] + return self._dependency_packages_info - def _prepare_current_addons_dist_items(self): + @property + def dependency_packages_items(self): + """Dependency packages as objects. + + Returns: + dict[str, DependencyItem]: Dependency packages as objects by name. + """ + + if self._dependency_packages_items is NOT_SET: + dependenc_package_items = {} + for item in self.dependency_packages_info: + item = DependencyItem.from_dict(item) + dependenc_package_items[item.name] = item + self._dependency_packages_items = dependenc_package_items + return self._dependency_packages_items + + @property + def dependency_package_item(self): + """Dependency package item that should be used by bundle. + + Returns: + Union[None, Dict[str, Any]]: None if bundle does not have + specified dependency package. + """ + + if self._dependency_package_item is NOT_SET: + dependency_package_item = None + bundle = self.bundle_to_use + if bundle is not None: + package_name = bundle.dependency_packages.get( + platform.system().lower() + ) + dependency_package_item = self.dependency_packages_items.get( + package_name) + self._dependency_package_item = dependency_package_item + return self._dependency_package_item + + def _prepare_current_addon_dist_items(self): addons_metadata = self.get_addons_metadata() - output = {} - for full_name, addon_info in self.addons_info.items(): - if not addon_info.require_distribution: + output = [] + addon_versions = {} + bundle = self.bundle_to_use + if bundle is not None: + addon_versions = bundle.addon_versions + for addon_name, addon_item in self.addon_items.items(): + addon_version = addon_versions.get(addon_name) + # Addon is not in bundle -> Skip + if addon_version is None: continue + + addon_version_item = addon_item.versions.get(addon_version) + # Addon version is not available in addons info + # - TODO handle this case (raise error, skip, store, report, ...) + if addon_version_item is None: + print( + f"Version '{addon_version}' of addon '{addon_name}'" + " is not available on server." + ) + continue + + if not addon_version_item.require_distribution: + continue + full_name = addon_version_item.full_name addon_dest = os.path.join(self._addons_dirpath, full_name) self.log.debug(f"Checking {full_name} in {addon_dest}") addon_in_metadata = ( - addon_info.name in addons_metadata - and addon_info.version in addons_metadata[addon_info.name] + addon_name in addons_metadata + and addon_version_item.version in addons_metadata[addon_name] ) if addon_in_metadata and os.path.isdir(addon_dest): self.log.debug( @@ -848,25 +762,32 @@ class AyonDistribution: downloader_data = { "type": "addon", - "name": addon_info.name, - "version": addon_info.version + "name": addon_name, + "version": addon_version } - output[full_name] = DistributionItem( + dist_item = DistributionItem( state, addon_dest, addon_dest, - addon_info.hash, + addon_version_item.hash, self._dist_factory, - list(addon_info.sources), + list(addon_version_item.sources), downloader_data, full_name, self.log ) + output.append({ + "dist_item": dist_item, + "addon_name": addon_name, + "addon_version": addon_version, + "addon_item": addon_item, + "addon_version_item": addon_version_item, + }) return output def _prepare_dependency_progress(self): - package = self.dependency_package + package = self.dependency_package_item if package is None or not package.require_distribution: return None @@ -898,20 +819,34 @@ class AyonDistribution: self.log, ) - def get_addons_dist_items(self): + def get_addon_dist_items(self): """Addon distribution items. These items describe source files required by addon to be available on machine. Each item may have 0-n source information from where can be obtained. If file is already available it's state will be 'UPDATED'. + Example output: + [ + { + "dist_item": DistributionItem, + "addon_name": str, + "addon_version": str, + "addon_item": AddonInfo, + "addon_version_item": AddonVersionInfo + }, { + ... + } + ] + Returns: - Dict[str, DistributionItem]: Distribution items by addon fullname. + list[dict[str, Any]]: Distribution items with addon version item. """ - if self._addons_dist_items is None: - self._addons_dist_items = self._prepare_current_addons_dist_items() - return self._addons_dist_items + if self._addon_dist_items is NOT_SET: + self._addon_dist_items = ( + self._prepare_current_addon_dist_items()) + return self._addon_dist_items def get_dependency_dist_item(self): """Dependency package distribution item. @@ -928,7 +863,7 @@ class AyonDistribution: does not have specified any dependency package. """ - if self._dependency_dist_item == -1: + if self._dependency_dist_item is NOT_SET: self._dependency_dist_item = self._prepare_dependency_progress() return self._dependency_dist_item @@ -1049,7 +984,8 @@ class AyonDistribution: self.update_dependency_metadata(package.name, data) addons_info = {} - for full_name, dist_item in self.get_addons_dist_items().items(): + for item in self.get_addon_dist_items(): + dist_item = item["dist_item"] if ( not dist_item.need_distribution or dist_item.state != UpdateState.UPDATED @@ -1059,10 +995,11 @@ class AyonDistribution: source_data = dist_item.used_source if not source_data: continue - addon_info = self.addons_info[full_name] - if addon_info.name not in addons_info: - addons_info[addon_info.name] = {} - addons_info[addon_info.name][addon_info.version] = { + + addon_name = item["addon_name"] + addon_version = item["addon_version"] + addons_info.setdefault(addon_name, {}) + addons_info[addon_name][addon_version] = { "source": source_data, "file_hash": dist_item.file_hash, "distributed_dt": stored_time @@ -1082,12 +1019,14 @@ class AyonDistribution: List[DistributionItem]: Distribution items required by server. """ - output = [] + output = [ + item["dist_item"] + for item in self.get_addon_dist_items() + ] dependency_dist_item = self.get_dependency_dist_item() if dependency_dist_item is not None: - output.append(dependency_dist_item) - for dist_item in self.get_addons_dist_items().values(): - output.append(dist_item) + output.insert(0, dependency_dist_item) + return output def distribute(self, threaded=False): @@ -1136,9 +1075,10 @@ class AyonDistribution: ): invalid.append("Dependency package") - for addon_name, dist_item in self.get_addons_dist_items().items(): + for item in self.get_addon_dist_items(): + dist_item = item["dist_item"] if dist_item.state != UpdateState.UPDATED: - invalid.append(addon_name) + invalid.append(item["addon_name"]) if not invalid: return @@ -1172,13 +1112,5 @@ class AyonDistribution: return output -def get_default_download_factory(): - download_factory = DownloadFactory() - download_factory.register_format(UrlType.FILESYSTEM, OSDownloader) - download_factory.register_format(UrlType.HTTP, HTTPDownloader) - download_factory.register_format(UrlType.SERVER, AyonServerDownloader) - return download_factory - - def cli(*args): raise NotImplementedError diff --git a/common/ayon_common/distribution/data_structures.py b/common/ayon_common/distribution/data_structures.py new file mode 100644 index 0000000000..19d3f6c744 --- /dev/null +++ b/common/ayon_common/distribution/data_structures.py @@ -0,0 +1,261 @@ +import attr +from enum import Enum + + +class UrlType(Enum): + HTTP = "http" + GIT = "git" + FILESYSTEM = "filesystem" + SERVER = "server" + + +@attr.s +class MultiPlatformValue(object): + windows = attr.ib(default=None) + linux = attr.ib(default=None) + darwin = attr.ib(default=None) + + +@attr.s +class SourceInfo(object): + type = attr.ib() + + +@attr.s +class LocalSourceInfo(SourceInfo): + path = attr.ib(default=attr.Factory(MultiPlatformValue)) + + +@attr.s +class WebSourceInfo(SourceInfo): + url = attr.ib(default=None) + headers = attr.ib(default=None) + filename = attr.ib(default=None) + + +@attr.s +class ServerSourceInfo(SourceInfo): + filename = attr.ib(default=None) + path = attr.ib(default=None) + + +def convert_source(source): + """Create source object from data information. + + Args: + source (Dict[str, any]): Information about source. + + Returns: + Union[None, SourceInfo]: Object with source information if type is + known. + """ + + source_type = source.get("type") + if not source_type: + return None + + if source_type == UrlType.FILESYSTEM.value: + return LocalSourceInfo( + type=source_type, + path=source["path"] + ) + + if source_type == UrlType.HTTP.value: + url = source["path"] + return WebSourceInfo( + type=source_type, + url=url, + headers=source.get("headers"), + filename=source.get("filename") + ) + + if source_type == UrlType.SERVER.value: + return ServerSourceInfo( + type=source_type, + filename=source.get("filename"), + path=source.get("path") + ) + + +def prepare_sources(src_sources): + sources = [] + unknown_sources = [] + for source in (src_sources or []): + dependency_source = convert_source(source) + if dependency_source is not None: + sources.append(dependency_source) + else: + print(f"Unknown source {source.get('type')}") + unknown_sources.append(source) + return sources, unknown_sources + + +@attr.s +class VersionData(object): + version_data = attr.ib(default=None) + + +@attr.s +class AddonVersionInfo(object): + version = attr.ib() + full_name = attr.ib() + title = attr.ib(default=None) + require_distribution = attr.ib(default=False) + sources = attr.ib(default=attr.Factory(list)) + unknown_sources = attr.ib(default=attr.Factory(list)) + hash = attr.ib(default=None) + + @classmethod + def from_dict( + cls, addon_name, addon_title, addon_version, version_data + ): + """Addon version info. + + Args: + addon_name (str): Name of addon. + addon_title (str): Title of addon. + addon_version (str): Version of addon. + version_data (dict[str, Any]): Addon version information from + server. + + Returns: + AddonVersionInfo: Addon version info. + """ + + full_name = f"{addon_name}_{addon_version}" + title = f"{addon_title} {addon_version}" + + source_info = version_data.get("clientSourceInfo") + require_distribution = source_info is not None + sources, unknown_sources = prepare_sources(source_info) + + return cls( + version=addon_version, + full_name=full_name, + require_distribution=require_distribution, + sources=sources, + unknown_sources=unknown_sources, + hash=version_data.get("hash"), + title=title + ) + + +@attr.s +class AddonInfo(object): + """Object matching json payload from Server""" + name = attr.ib() + versions = attr.ib(default=attr.Factory(dict)) + title = attr.ib(default=None) + description = attr.ib(default=None) + license = attr.ib(default=None) + authors = attr.ib(default=None) + + @classmethod + def from_dict(cls, data): + """Addon info by available versions. + + Args: + data (dict[str, Any]): Addon information from server. Should + contain information about every version under 'versions'. + + Returns: + AddonInfo: Addon info with available versions. + """ + + # server payload contains info about all versions + addon_name = data["name"] + title = data.get("title") or addon_name + + src_versions = data.get("versions") or {} + dst_versions = { + addon_version: AddonVersionInfo.from_dict( + addon_name, title, addon_version, version_data + ) + for addon_version, version_data in src_versions.items() + } + return cls( + name=addon_name, + versions=dst_versions, + description=data.get("description"), + title=data.get("title") or addon_name, + license=data.get("license"), + authors=data.get("authors") + ) + + +@attr.s +class DependencyItem(object): + """Object matching payload from Server about single dependency package""" + name = attr.ib() + platform_name = attr.ib() + checksum = attr.ib() + sources = attr.ib(default=attr.Factory(list)) + unknown_sources = attr.ib(default=attr.Factory(list)) + source_addons = attr.ib(default=attr.Factory(dict)) + python_modules = attr.ib(default=attr.Factory(dict)) + + @classmethod + def from_dict(cls, package): + sources, unknown_sources = prepare_sources(package.get("sources")) + return cls( + name=package["name"], + platform_name=package["platform"], + sources=sources, + unknown_sources=unknown_sources, + checksum=package["checksum"], + source_addons=package["sourceAddons"], + python_modules=package["pythonModules"] + ) + + +@attr.s +class Installer: + version = attr.ib() + filename = attr.ib() + platform_name = attr.ib() + size = attr.ib() + checksum = attr.ib() + python_version = attr.ib() + python_modules = attr.ib() + sources = attr.ib(default=attr.Factory(list)) + unknown_sources = attr.ib(default=attr.Factory(list)) + + @classmethod + def from_dict(cls, installer_info): + sources, unknown_sources = prepare_sources( + installer_info.get("sources")) + + return cls( + version=installer_info["version"], + filename=installer_info["filename"], + platform_name=installer_info["platform"], + size=installer_info["size"], + sources=sources, + unknown_sources=unknown_sources, + checksum=installer_info["checksum"], + python_version=installer_info["pythonVersion"], + python_modules=installer_info["pythonModules"] + ) + + +@attr.s +class Bundle: + """Class representing bundle information.""" + + name = attr.ib() + installer_version = attr.ib() + addon_versions = attr.ib(default=attr.Factory(dict)) + dependency_packages = attr.ib(default=attr.Factory(dict)) + is_production = attr.ib(default=False) + is_staging = attr.ib(default=False) + + @classmethod + def from_dict(cls, data): + return cls( + name=data["name"], + installer_version=data.get("installerVersion"), + addon_versions=data.get("addons", {}), + dependency_packages=data.get("dependencyPackages", {}), + is_production=data["isProduction"], + is_staging=data["isStaging"], + ) diff --git a/common/ayon_common/distribution/downloaders.py b/common/ayon_common/distribution/downloaders.py new file mode 100644 index 0000000000..23280176c3 --- /dev/null +++ b/common/ayon_common/distribution/downloaders.py @@ -0,0 +1,250 @@ +import os +import logging +import platform +from abc import ABCMeta, abstractmethod + +import ayon_api + +from .file_handler import RemoteFileHandler +from .data_structures import UrlType + + +class SourceDownloader(metaclass=ABCMeta): + """Abstract class for source downloader.""" + + log = logging.getLogger(__name__) + + @classmethod + @abstractmethod + def download(cls, source, destination_dir, data, transfer_progress): + """Returns url of downloaded addon zip file. + + Tranfer progress can be ignored, in that case file transfer won't + be shown as 0-100% but as 'running'. First step should be to set + destination content size and then add transferred chunk sizes. + + Args: + source (dict): {type:"http", "url":"https://} ...} + destination_dir (str): local folder to unzip + data (dict): More information about download content. Always have + 'type' key in. + transfer_progress (ayon_api.TransferProgress): Progress of + transferred (copy/download) content. + + Returns: + (str) local path to addon zip file + """ + + pass + + @classmethod + @abstractmethod + def cleanup(cls, source, destination_dir, data): + """Cleanup files when distribution finishes or crashes. + + Cleanup e.g. temporary files (downloaded zip) or other related stuff + to downloader. + """ + + pass + + @classmethod + def check_hash(cls, addon_path, addon_hash, hash_type="sha256"): + """Compares 'hash' of downloaded 'addon_url' file. + + Args: + addon_path (str): Local path to addon file. + addon_hash (str): Hash of downloaded file. + hash_type (str): Type of hash. + + Raises: + ValueError if hashes doesn't match + """ + + if not os.path.exists(addon_path): + raise ValueError(f"{addon_path} doesn't exist.") + if not RemoteFileHandler.check_integrity( + addon_path, addon_hash, hash_type=hash_type + ): + raise ValueError(f"{addon_path} doesn't match expected hash.") + + @classmethod + def unzip(cls, addon_zip_path, destination_dir): + """Unzips local 'addon_zip_path' to 'destination'. + + Args: + addon_zip_path (str): local path to addon zip file + destination_dir (str): local folder to unzip + """ + + RemoteFileHandler.unzip(addon_zip_path, destination_dir) + os.remove(addon_zip_path) + + +class OSDownloader(SourceDownloader): + """Downloader using files from file drive.""" + + @classmethod + def download(cls, source, destination_dir, data, transfer_progress): + # OS doesn't need to download, unzip directly + addon_url = source["path"].get(platform.system().lower()) + if not os.path.exists(addon_url): + raise ValueError(f"{addon_url} is not accessible") + return addon_url + + @classmethod + def cleanup(cls, source, destination_dir, data): + # Nothing to do - download does not copy anything + pass + + +class HTTPDownloader(SourceDownloader): + """Downloader using http or https protocol.""" + + CHUNK_SIZE = 100000 + + @staticmethod + def get_filename(source): + source_url = source["url"] + filename = source.get("filename") + if not filename: + filename = os.path.basename(source_url) + basename, ext = os.path.splitext(filename) + allowed_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS) + if ext.lower().lstrip(".") not in allowed_exts: + filename = f"{basename}.zip" + return filename + + @classmethod + def download(cls, source, destination_dir, data, transfer_progress): + source_url = source["url"] + cls.log.debug(f"Downloading {source_url} to {destination_dir}") + headers = source.get("headers") + filename = cls.get_filename(source) + + # TODO use transfer progress + RemoteFileHandler.download_url( + source_url, + destination_dir, + filename, + headers=headers + ) + + return os.path.join(destination_dir, filename) + + @classmethod + def cleanup(cls, source, destination_dir, data): + filename = cls.get_filename(source) + filepath = os.path.join(destination_dir, filename) + if os.path.exists(filepath) and os.path.isfile(filepath): + os.remove(filepath) + + +class AyonServerDownloader(SourceDownloader): + """Downloads static resource file from AYON Server. + + Expects filled env var AYON_SERVER_URL. + """ + + CHUNK_SIZE = 8192 + + @classmethod + def download(cls, source, destination_dir, data, transfer_progress): + path = source["path"] + filename = source["filename"] + if path and not filename: + filename = path.split("/")[-1] + + cls.log.debug(f"Downloading {filename} to {destination_dir}") + + _, ext = os.path.splitext(filename) + ext = ext.lower().lstrip(".") + valid_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS) + if ext not in valid_exts: + raise ValueError(( + f"Invalid file extension \"{ext}\"." + f" Expected {', '.join(valid_exts)}" + )) + + if path: + filepath = os.path.join(destination_dir, filename) + return ayon_api.download_file( + path, + filepath, + chunk_size=cls.CHUNK_SIZE, + progress=transfer_progress + ) + + # dst_filepath = os.path.join(destination_dir, filename) + if data["type"] == "dependency_package": + return ayon_api.download_dependency_package( + data["name"], + destination_dir, + filename, + platform_name=data["platform"], + chunk_size=cls.CHUNK_SIZE, + progress=transfer_progress + ) + + if data["type"] == "addon": + return ayon_api.download_addon_private_file( + data["name"], + data["version"], + filename, + destination_dir, + chunk_size=cls.CHUNK_SIZE, + progress=transfer_progress + ) + + raise ValueError(f"Unknown type to download \"{data['type']}\"") + + @classmethod + def cleanup(cls, source, destination_dir, data): + filename = source["filename"] + filepath = os.path.join(destination_dir, filename) + if os.path.exists(filepath) and os.path.isfile(filepath): + os.remove(filepath) + + +class DownloadFactory: + """Factory for downloaders.""" + + def __init__(self): + self._downloaders = {} + + def register_format(self, downloader_type, downloader): + """Register downloader for download type. + + Args: + downloader_type (UrlType): Type of source. + downloader (SourceDownloader): Downloader which cares about + download, hash check and unzipping. + """ + + self._downloaders[downloader_type.value] = downloader + + def get_downloader(self, downloader_type): + """Registered downloader for type. + + Args: + downloader_type (UrlType): Type of source. + + Returns: + SourceDownloader: Downloader object which should care about file + distribution. + + Raises: + ValueError: If type does not have registered downloader. + """ + + if downloader := self._downloaders.get(downloader_type): + return downloader() + raise ValueError(f"{downloader_type} not implemented") + + +def get_default_download_factory(): + download_factory = DownloadFactory() + download_factory.register_format(UrlType.FILESYSTEM, OSDownloader) + download_factory.register_format(UrlType.HTTP, HTTPDownloader) + download_factory.register_format(UrlType.SERVER, AyonServerDownloader) + return download_factory diff --git a/common/ayon_common/distribution/file_handler.py b/common/ayon_common/distribution/file_handler.py index a666b014f0..07f6962c98 100644 --- a/common/ayon_common/distribution/file_handler.py +++ b/common/ayon_common/distribution/file_handler.py @@ -9,21 +9,23 @@ import hashlib import tarfile import zipfile +import requests -USER_AGENT = "openpype" +USER_AGENT = "AYON-launcher" class RemoteFileHandler: """Download file from url, might be GDrive shareable link""" - IMPLEMENTED_ZIP_FORMATS = ['zip', 'tar', 'tgz', - 'tar.gz', 'tar.xz', 'tar.bz2'] + IMPLEMENTED_ZIP_FORMATS = { + "zip", "tar", "tgz", "tar.gz", "tar.xz", "tar.bz2" + } @staticmethod def calculate_md5(fpath, chunk_size=10000): md5 = hashlib.md5() - with open(fpath, 'rb') as f: - for chunk in iter(lambda: f.read(chunk_size), b''): + with open(fpath, "rb") as f: + for chunk in iter(lambda: f.read(chunk_size), b""): md5.update(chunk) return md5.hexdigest() @@ -45,7 +47,7 @@ class RemoteFileHandler: h = hashlib.sha256() b = bytearray(128 * 1024) mv = memoryview(b) - with open(fpath, 'rb', buffering=0) as f: + with open(fpath, "rb", buffering=0) as f: for n in iter(lambda: f.readinto(mv), 0): h.update(mv[:n]) return h.hexdigest() @@ -69,21 +71,25 @@ class RemoteFileHandler: @staticmethod def download_url( - url, root, filename=None, - sha256=None, max_redirect_hops=3, headers=None + url, + root, + filename=None, + max_redirect_hops=3, + headers=None ): - """Download a file from a url and place it in root. + """Download a file from url and place it in root. + Args: url (str): URL to download file from root (str): Directory to place downloaded file in filename (str, optional): Name to save the file under. If None, use the basename of the URL - sha256 (str, optional): sha256 checksum of the download. - If None, do not check - max_redirect_hops (int, optional): Maximum number of redirect + max_redirect_hops (Optional[int]): Maximum number of redirect hops allowed - headers (dict): additional required headers - Authentication etc.. + headers (Optional[dict[str, str]]): Additional required headers + - Authentication etc.. """ + root = os.path.expanduser(root) if not filename: filename = os.path.basename(url) @@ -91,59 +97,44 @@ class RemoteFileHandler: os.makedirs(root, exist_ok=True) - # check if file is already present locally - if RemoteFileHandler.check_integrity(fpath, - sha256, hash_type="sha256"): - print(f"Using downloaded and verified file: {fpath}") - return - # expand redirect chain if needed - url = RemoteFileHandler._get_redirect_url(url, - max_hops=max_redirect_hops, - headers=headers) + url = RemoteFileHandler._get_redirect_url( + url, max_hops=max_redirect_hops, headers=headers) # check if file is located on Google Drive file_id = RemoteFileHandler._get_google_drive_file_id(url) if file_id is not None: return RemoteFileHandler.download_file_from_google_drive( - file_id, root, filename, sha256) + file_id, root, filename) # download the file try: print(f"Downloading {url} to {fpath}") RemoteFileHandler._urlretrieve(url, fpath, headers=headers) - except (urllib.error.URLError, IOError) as e: - if url[:5] == "https": - url = url.replace("https:", "http:") - print(( - "Failed download. Trying https -> http instead." - f" Downloading {url} to {fpath}" - )) - RemoteFileHandler._urlretrieve(url, fpath, - headers=headers) - else: - raise e + except (urllib.error.URLError, IOError) as exc: + if url[:5] != "https": + raise exc - # check integrity of downloaded file - if not RemoteFileHandler.check_integrity(fpath, - sha256, hash_type="sha256"): - raise RuntimeError("File not found or corrupted.") + url = url.replace("https:", "http:") + print(( + "Failed download. Trying https -> http instead." + f" Downloading {url} to {fpath}" + )) + RemoteFileHandler._urlretrieve(url, fpath, headers=headers) @staticmethod - def download_file_from_google_drive(file_id, root, - filename=None, - sha256=None): + def download_file_from_google_drive( + file_id, root, filename=None + ): """Download a Google Drive file from and place it in root. Args: file_id (str): id of file to be downloaded root (str): Directory to place downloaded file in filename (str, optional): Name to save the file under. If None, use the id of the file. - sha256 (str, optional): sha256 checksum of the download. - If None, do not check """ # Based on https://stackoverflow.com/questions/38511444/python-download-files-from-google-drive-using-url # noqa - import requests + url = "https://docs.google.com/uc?export=download" root = os.path.expanduser(root) @@ -153,17 +144,16 @@ class RemoteFileHandler: os.makedirs(root, exist_ok=True) - if os.path.isfile(fpath) and RemoteFileHandler.check_integrity( - fpath, sha256, hash_type="sha256"): - print('Using downloaded and verified file: ' + fpath) + if os.path.isfile(fpath) and RemoteFileHandler.check_integrity(fpath): + print(f"Using downloaded and verified file: {fpath}") else: session = requests.Session() - response = session.get(url, params={'id': file_id}, stream=True) + response = session.get(url, params={"id": file_id}, stream=True) token = RemoteFileHandler._get_confirm_token(response) if token: - params = {'id': file_id, 'confirm': token} + params = {"id": file_id, "confirm": token} response = session.get(url, params=params, stream=True) response_content_generator = response.iter_content(32768) @@ -191,28 +181,28 @@ class RemoteFileHandler: destination_path = os.path.dirname(path) _, archive_type = os.path.splitext(path) - archive_type = archive_type.lstrip('.') + archive_type = archive_type.lstrip(".") - if archive_type in ['zip']: - print("Unzipping {}->{}".format(path, destination_path)) + if archive_type in ["zip"]: + print(f"Unzipping {path}->{destination_path}") zip_file = zipfile.ZipFile(path) zip_file.extractall(destination_path) zip_file.close() elif archive_type in [ - 'tar', 'tgz', 'tar.gz', 'tar.xz', 'tar.bz2' + "tar", "tgz", "tar.gz", "tar.xz", "tar.bz2" ]: - print("Unzipping {}->{}".format(path, destination_path)) - if archive_type == 'tar': - tar_type = 'r:' - elif archive_type.endswith('xz'): - tar_type = 'r:xz' - elif archive_type.endswith('gz'): - tar_type = 'r:gz' - elif archive_type.endswith('bz2'): - tar_type = 'r:bz2' + print(f"Unzipping {path}->{destination_path}") + if archive_type == "tar": + tar_type = "r:" + elif archive_type.endswith("xz"): + tar_type = "r:xz" + elif archive_type.endswith("gz"): + tar_type = "r:gz" + elif archive_type.endswith("bz2"): + tar_type = "r:bz2" else: - tar_type = 'r:*' + tar_type = "r:*" try: tar_file = tarfile.open(path, tar_type) except tarfile.ReadError: @@ -229,9 +219,8 @@ class RemoteFileHandler: chunk_size = chunk_size or 8192 with open(filename, "wb") as fh: with urllib.request.urlopen( - urllib.request.Request(url, - headers=final_headers)) \ - as response: + urllib.request.Request(url, headers=final_headers) + ) as response: for chunk in iter(lambda: response.read(chunk_size), ""): if not chunk: break @@ -245,12 +234,12 @@ class RemoteFileHandler: final_headers.update(headers) for _ in range(max_hops + 1): with urllib.request.urlopen( - urllib.request.Request(url, - headers=final_headers)) as response: + urllib.request.Request(url, headers=final_headers) + ) as response: if response.url == url or response.url is None: return url - url = response.url + return response.url else: raise RecursionError( f"Request to {initial_url} exceeded {max_hops} redirects. " @@ -260,7 +249,7 @@ class RemoteFileHandler: @staticmethod def _get_confirm_token(response): for key, value in response.cookies.items(): - if key.startswith('download_warning'): + if key.startswith("download_warning"): return value # handle antivirus warning for big zips diff --git a/common/ayon_common/distribution/tests/test_addon_distributtion.py b/common/ayon_common/distribution/tests/test_addon_distributtion.py index 22a347f3eb..3e7bd1bc6a 100644 --- a/common/ayon_common/distribution/tests/test_addon_distributtion.py +++ b/common/ayon_common/distribution/tests/test_addon_distributtion.py @@ -1,20 +1,33 @@ -import pytest -import attr +import os +import sys +import copy import tempfile -from common.ayon_common.distribution.addon_distribution import ( + +import attr +import pytest + +current_dir = os.path.dirname(os.path.abspath(__file__)) +root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", "..", "..")) +sys.path.append(root_dir) + +from common.ayon_common.distribution.downloaders import ( DownloadFactory, OSDownloader, HTTPDownloader, - AddonInfo, - AyonDistribution, - UpdateState ) -from common.ayon_common.distribution.addon_info import UrlType +from common.ayon_common.distribution.control import ( + AyonDistribution, + UpdateState, +) +from common.ayon_common.distribution.data_structures import ( + AddonInfo, + UrlType, +) @pytest.fixture -def addon_download_factory(): +def download_factory(): addon_downloader = DownloadFactory() addon_downloader.register_format(UrlType.FILESYSTEM, OSDownloader) addon_downloader.register_format(UrlType.HTTP, HTTPDownloader) @@ -23,65 +36,84 @@ def addon_download_factory(): @pytest.fixture -def http_downloader(addon_download_factory): - yield addon_download_factory.get_downloader(UrlType.HTTP.value) +def http_downloader(download_factory): + yield download_factory.get_downloader(UrlType.HTTP.value) @pytest.fixture def temp_folder(): - yield tempfile.mkdtemp() + yield tempfile.mkdtemp(prefix="ayon_test_") + + +@pytest.fixture +def sample_bundles(): + yield { + "bundles": [ + { + "name": "TestBundle", + "createdAt": "2023-06-29T00:00:00.0+00:00", + "installerVersion": None, + "addons": { + "slack": "1.0.0" + }, + "dependencyPackages": {}, + "isProduction": True, + "isStaging": False + } + ], + "productionBundle": "TestBundle", + "stagingBundle": None + } @pytest.fixture def sample_addon_info(): - addon_info = { - "versions": { + yield { + "name": "slack", + "title": "Slack addon", + "versions": { "1.0.0": { - "clientPyproject": { - "tool": { - "poetry": { + "hasSettings": True, + "hasSiteSettings": False, + "clientPyproject": { + "tool": { + "poetry": { "dependencies": { - "nxtools": "^1.6", - "orjson": "^3.6.7", - "typer": "^0.4.1", - "email-validator": "^1.1.3", - "python": "^3.10", - "fastapi": "^0.73.0" + "nxtools": "^1.6", + "orjson": "^3.6.7", + "typer": "^0.4.1", + "email-validator": "^1.1.3", + "python": "^3.10", + "fastapi": "^0.73.0" } - } - } - }, - "hasSettings": True, - "clientSourceInfo": [ - { - "type": "http", - "path": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing", # noqa - "filename": "dummy.zip" - }, - { - "type": "filesystem", - "path": { - "windows": ["P:/sources/some_file.zip", - "W:/sources/some_file.zip"], # noqa - "linux": ["/mnt/srv/sources/some_file.zip"], - "darwin": ["/Volumes/srv/sources/some_file.zip"] - } - } - ], - "frontendScopes": { - "project": { - "sidebar": "hierarchy" - } - } + } + } + }, + "clientSourceInfo": [ + { + "type": "http", + "path": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing", # noqa + "filename": "dummy.zip" + }, + { + "type": "filesystem", + "path": { + "windows": "P:/sources/some_file.zip", + "linux": "/mnt/srv/sources/some_file.zip", + "darwin": "/Volumes/srv/sources/some_file.zip" + } + } + ], + "frontendScopes": { + "project": { + "sidebar": "hierarchy", + } + }, + "hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658" # noqa } - }, - "description": "", - "title": "Slack addon", - "name": "openpype_slack", - "productionVersion": "1.0.0", - "hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658" # noqa + }, + "description": "" } - yield addon_info def test_register(printer): @@ -103,21 +135,16 @@ def test_get_downloader(printer, download_factory): def test_addon_info(printer, sample_addon_info): """Tests parsing of expected payload from v4 server into AadonInfo.""" valid_minimum = { - "name": "openpype_slack", - "productionVersion": "1.0.0", - "versions": { - "1.0.0": { - "clientSourceInfo": [ - { - "type": "filesystem", - "path": { - "windows": [ - "P:/sources/some_file.zip", - "W:/sources/some_file.zip"], - "linux": [ - "/mnt/srv/sources/some_file.zip"], - "darwin": [ - "/Volumes/srv/sources/some_file.zip"] # noqa + "name": "slack", + "versions": { + "1.0.0": { + "clientSourceInfo": [ + { + "type": "filesystem", + "path": { + "windows": "P:/sources/some_file.zip", + "linux": "/mnt/srv/sources/some_file.zip", + "darwin": "/Volumes/srv/sources/some_file.zip" } } ] @@ -127,18 +154,10 @@ def test_addon_info(printer, sample_addon_info): assert AddonInfo.from_dict(valid_minimum), "Missing required fields" - valid_minimum["versions"].pop("1.0.0") - with pytest.raises(KeyError): - assert not AddonInfo.from_dict(valid_minimum), "Must fail without version data" # noqa - - valid_minimum.pop("productionVersion") - assert not AddonInfo.from_dict( - valid_minimum), "none if not productionVersion" # noqa - addon = AddonInfo.from_dict(sample_addon_info) assert addon, "Should be created" - assert addon.name == "openpype_slack", "Incorrect name" - assert addon.version == "1.0.0", "Incorrect version" + assert addon.name == "slack", "Incorrect name" + assert "1.0.0" in addon.versions, "Version is not in versions" with pytest.raises(TypeError): assert addon["name"], "Dict approach not implemented" @@ -147,37 +166,83 @@ def test_addon_info(printer, sample_addon_info): assert addon_as_dict["name"], "Dict approach should work" -def test_update_addon_state(printer, sample_addon_info, - temp_folder, download_factory): +def _get_dist_item(dist_items, name, version): + final_dist_info = next( + ( + dist_info + for dist_info in dist_items + if ( + dist_info["addon_name"] == name + and dist_info["addon_version"] == version + ) + ), + {} + ) + return final_dist_info["dist_item"] + + +def test_update_addon_state( + printer, sample_addon_info, temp_folder, download_factory, sample_bundles +): """Tests possible cases of addon update.""" - addon_info = AddonInfo.from_dict(sample_addon_info) - orig_hash = addon_info.hash + + addon_version = list(sample_addon_info["versions"])[0] + broken_addon_info = copy.deepcopy(sample_addon_info) # Cause crash because of invalid hash - addon_info.hash = "brokenhash" + broken_addon_info["versions"][addon_version]["hash"] = "brokenhash" distribution = AyonDistribution( - temp_folder, temp_folder, download_factory, [addon_info], None + addon_dirpath=temp_folder, + dependency_dirpath=temp_folder, + dist_factory=download_factory, + addons_info=[broken_addon_info], + dependency_packages_info=[], + bundles_info=sample_bundles ) distribution.distribute() - dist_items = distribution.get_addons_dist_items() - slack_state = dist_items["openpype_slack_1.0.0"].state + dist_items = distribution.get_addon_dist_items() + slack_dist_item = _get_dist_item( + dist_items, + sample_addon_info["name"], + addon_version + ) + slack_state = slack_dist_item.state assert slack_state == UpdateState.UPDATE_FAILED, ( "Update should have failed because of wrong hash") # Fix cache and validate if was updated - addon_info.hash = orig_hash distribution = AyonDistribution( - temp_folder, temp_folder, download_factory, [addon_info], None + addon_dirpath=temp_folder, + dependency_dirpath=temp_folder, + dist_factory=download_factory, + addons_info=[sample_addon_info], + dependency_packages_info=[], + bundles_info=sample_bundles ) distribution.distribute() - dist_items = distribution.get_addons_dist_items() - assert dist_items["openpype_slack_1.0.0"].state == UpdateState.UPDATED, ( + dist_items = distribution.get_addon_dist_items() + slack_dist_item = _get_dist_item( + dist_items, + sample_addon_info["name"], + addon_version + ) + assert slack_dist_item.state == UpdateState.UPDATED, ( "Addon should have been updated") # Is UPDATED without calling distribute distribution = AyonDistribution( - temp_folder, temp_folder, download_factory, [addon_info], None + addon_dirpath=temp_folder, + dependency_dirpath=temp_folder, + dist_factory=download_factory, + addons_info=[sample_addon_info], + dependency_packages_info=[], + bundles_info=sample_bundles ) - dist_items = distribution.get_addons_dist_items() - assert dist_items["openpype_slack_1.0.0"].state == UpdateState.UPDATED, ( + dist_items = distribution.get_addon_dist_items() + slack_dist_item = _get_dist_item( + dist_items, + sample_addon_info["name"], + addon_version + ) + assert slack_dist_item.state == UpdateState.UPDATED, ( "Addon should already exist") diff --git a/common/ayon_common/distribution/ui/missing_bundle_window.py b/common/ayon_common/distribution/ui/missing_bundle_window.py new file mode 100644 index 0000000000..ae7a6a2976 --- /dev/null +++ b/common/ayon_common/distribution/ui/missing_bundle_window.py @@ -0,0 +1,146 @@ +import sys + +from qtpy import QtWidgets, QtGui + +from ayon_common import is_staging_enabled +from ayon_common.resources import ( + get_icon_path, + load_stylesheet, +) +from ayon_common.ui_utils import get_qt_app + + +class MissingBundleWindow(QtWidgets.QDialog): + default_width = 410 + default_height = 170 + + def __init__( + self, url=None, bundle_name=None, use_staging=None, parent=None + ): + super().__init__(parent) + + icon_path = get_icon_path() + icon = QtGui.QIcon(icon_path) + self.setWindowIcon(icon) + self.setWindowTitle("Missing Bundle") + + self._url = url + self._bundle_name = bundle_name + self._use_staging = use_staging + self._first_show = True + + info_label = QtWidgets.QLabel("", self) + info_label.setWordWrap(True) + + btns_widget = QtWidgets.QWidget(self) + confirm_btn = QtWidgets.QPushButton("Exit", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(confirm_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(info_label, 0) + main_layout.addStretch(1) + main_layout.addWidget(btns_widget, 0) + + confirm_btn.clicked.connect(self._on_confirm_click) + + self._info_label = info_label + self._confirm_btn = confirm_btn + + self._update_label() + + def set_url(self, url): + if url == self._url: + return + self._url = url + self._update_label() + + def set_bundle_name(self, bundle_name): + if bundle_name == self._bundle_name: + return + self._bundle_name = bundle_name + self._update_label() + + def set_use_staging(self, use_staging): + if self._use_staging == use_staging: + return + self._use_staging = use_staging + self._update_label() + + def showEvent(self, event): + super().showEvent(event) + if self._first_show: + self._first_show = False + self._on_first_show() + self._recalculate_sizes() + + def resizeEvent(self, event): + super().resizeEvent(event) + self._recalculate_sizes() + + def _recalculate_sizes(self): + hint = self._confirm_btn.sizeHint() + new_width = max((hint.width(), hint.height() * 3)) + self._confirm_btn.setMinimumWidth(new_width) + + def _on_first_show(self): + self.setStyleSheet(load_stylesheet()) + self.resize(self.default_width, self.default_height) + + def _on_confirm_click(self): + self.accept() + self.close() + + def _update_label(self): + self._info_label.setText(self._get_label()) + + def _get_label(self): + url_part = f" {self._url}" if self._url else "" + + if self._bundle_name: + return ( + f"Requested release bundle {self._bundle_name}" + f" is not available on server{url_part}." + "

Try to restart AYON desktop launcher. Please" + " contact your administrator if issue persist." + ) + mode = "staging" if self._use_staging else "production" + return ( + f"No release bundle is set as {mode} on the AYON" + f" server{url_part} so there is nothing to launch." + "

Please contact your administrator" + " to resolve the issue." + ) + + +def main(): + """Show message that server does not have set bundle to use. + + It is possible to pass url as argument to show it in the message. To use + this feature, pass `--url ` as argument to this script. + """ + + url = None + bundle_name = None + if "--url" in sys.argv: + url_index = sys.argv.index("--url") + 1 + if url_index < len(sys.argv): + url = sys.argv[url_index] + + if "--bundle" in sys.argv: + bundle_index = sys.argv.index("--bundle") + 1 + if bundle_index < len(sys.argv): + bundle_name = sys.argv[bundle_index] + + use_staging = is_staging_enabled() + app = get_qt_app() + window = MissingBundleWindow(url, bundle_name, use_staging) + window.show() + app.exec_() + + +if __name__ == "__main__": + main() diff --git a/common/ayon_common/distribution/utils.py b/common/ayon_common/distribution/utils.py new file mode 100644 index 0000000000..a8b755707a --- /dev/null +++ b/common/ayon_common/distribution/utils.py @@ -0,0 +1,90 @@ +import os +import subprocess + +from ayon_common.utils import get_ayon_appdirs, get_ayon_launch_args + + +def get_local_dir(*subdirs): + """Get product directory in user's home directory. + + Each user on machine have own local directory where are downloaded updates, + addons etc. + + Returns: + str: Path to product local directory. + """ + + if not subdirs: + raise ValueError("Must fill dir_name if nothing else provided!") + + local_dir = get_ayon_appdirs(*subdirs) + if not os.path.isdir(local_dir): + try: + os.makedirs(local_dir) + except Exception: # TODO fix exception + raise RuntimeError(f"Cannot create {local_dir}") + + return local_dir + + +def get_addons_dir(): + """Directory where addon packages are stored. + + Path to addons is defined using python module 'appdirs' which + + The path is stored into environment variable 'AYON_ADDONS_DIR'. + Value of environment variable can be overriden, but we highly recommended + to use that option only for development purposes. + + Returns: + str: Path to directory where addons should be downloaded. + """ + + addons_dir = os.environ.get("AYON_ADDONS_DIR") + if not addons_dir: + addons_dir = get_local_dir("addons") + os.environ["AYON_ADDONS_DIR"] = addons_dir + return addons_dir + + +def get_dependencies_dir(): + """Directory where dependency packages are stored. + + Path to addons is defined using python module 'appdirs' which + + The path is stored into environment variable 'AYON_DEPENDENCIES_DIR'. + Value of environment variable can be overriden, but we highly recommended + to use that option only for development purposes. + + Returns: + str: Path to directory where dependency packages should be downloaded. + """ + + dependencies_dir = os.environ.get("AYON_DEPENDENCIES_DIR") + if not dependencies_dir: + dependencies_dir = get_local_dir("dependency_packages") + os.environ["AYON_DEPENDENCIES_DIR"] = dependencies_dir + return dependencies_dir + + +def show_missing_bundle_information(url, bundle_name=None): + """Show missing bundle information window. + + This function should be called when server does not have set bundle for + production or staging, or when bundle that should be used is not available + on server. + + Using subprocess to show the dialog. Is blocking and is waiting until + dialog is closed. + + Args: + url (str): Server url where bundle is not set. + bundle_name (Optional[str]): Name of bundle that was not found. + """ + + ui_dir = os.path.join(os.path.dirname(__file__), "ui") + script_path = os.path.join(ui_dir, "missing_bundle_window.py") + args = get_ayon_launch_args(script_path, "--skip-bootstrap", "--url", url) + if bundle_name: + args.extend(["--bundle", bundle_name]) + subprocess.call(args) diff --git a/common/ayon_common/resources/__init__.py b/common/ayon_common/resources/__init__.py index 21e5fef6b2..2b516feff3 100644 --- a/common/ayon_common/resources/__init__.py +++ b/common/ayon_common/resources/__init__.py @@ -1,5 +1,7 @@ import os +from ayon_common.utils import is_staging_enabled + RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -10,7 +12,7 @@ def get_resource_path(*args): def get_icon_path(): - if os.environ.get("OPENPYPE_USE_STAGING") == "1": + if is_staging_enabled(): return get_resource_path("AYON_staging.png") return get_resource_path("AYON.png") diff --git a/common/ayon_common/resources/stylesheet.css b/common/ayon_common/resources/stylesheet.css index 732f44f6d1..01e664e9e8 100644 --- a/common/ayon_common/resources/stylesheet.css +++ b/common/ayon_common/resources/stylesheet.css @@ -81,4 +81,4 @@ QLineEdit[state="invalid"] { } #LikeDisabledInput:focus { border-color: #373D48; -} \ No newline at end of file +} diff --git a/common/ayon_common/connection/ui/lib.py b/common/ayon_common/ui_utils.py similarity index 100% rename from common/ayon_common/connection/ui/lib.py rename to common/ayon_common/ui_utils.py diff --git a/common/ayon_common/utils.py b/common/ayon_common/utils.py index bbf7f01607..d0638e552f 100644 --- a/common/ayon_common/utils.py +++ b/common/ayon_common/utils.py @@ -1,6 +1,9 @@ import os +import sys import appdirs +IS_BUILT_APPLICATION = getattr(sys, "frozen", False) + def get_ayon_appdirs(*args): """Local app data directory of AYON client. @@ -18,8 +21,23 @@ def get_ayon_appdirs(*args): ) +def is_staging_enabled(): + """Check if staging is enabled. + + Returns: + bool: True if staging is enabled. + """ + + return os.getenv("AYON_USE_STAGING") == "1" + + def _create_local_site_id(): - """Create a local site identifier.""" + """Create a local site identifier. + + Returns: + str: Randomly generated site id. + """ + from coolname import generate_slug new_id = generate_slug(3) @@ -33,6 +51,9 @@ def get_local_site_id(): """Get local site identifier. Site id is created if does not exist yet. + + Returns: + str: Site id. """ # used for background syncing @@ -50,3 +71,20 @@ def get_local_site_id(): with open(site_id_path, "w") as stream: stream.write(site_id) return site_id + + +def get_ayon_launch_args(*args): + """Launch arguments that can be used to launch ayon process. + + Args: + *args (str): Additional arguments. + + Returns: + list[str]: Launch arguments. + """ + + output = [sys.executable] + if not IS_BUILT_APPLICATION: + output.append(sys.argv[0]) + output.extend(args) + return output diff --git a/openpype/modules/base.py b/openpype/modules/base.py index ab18c15f9a..24ddc97ac0 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Base class for Pype Modules.""" +import copy import os import sys import json @@ -319,7 +320,35 @@ def _get_ayon_addons_information(): List[Dict[str, Any]]: List of addon information to use. """ - return ayon_api.get_addons_info()["addons"] + output = [] + bundle_name = os.getenv("AYON_BUNDLE_NAME") + bundles = ayon_api.get_bundles()["bundles"] + final_bundle = next( + ( + bundle + for bundle in bundles + if bundle["name"] == bundle_name + ), + None + ) + if final_bundle is None: + return output + + bundle_addons = final_bundle["addons"] + addons = ayon_api.get_addons_info()["addons"] + for addon in addons: + name = addon["name"] + versions = addon.get("versions") + addon_version = bundle_addons.get(name) + if addon_version is None or not versions: + continue + version = versions.get(addon_version) + if version: + version = copy.deepcopy(version) + version["name"] = name + version["version"] = addon_version + output.append(version) + return output def _load_ayon_addons(openpype_modules, modules_key, log): @@ -354,15 +383,9 @@ def _load_ayon_addons(openpype_modules, modules_key, log): )) return v3_addons_to_skip - version_key = ( - "stagingVersion" if is_staging_enabled() - else "productionVersion" - ) for addon_info in addons_info: addon_name = addon_info["name"] - addon_version = addon_info.get(version_key) - if not addon_version: - continue + addon_version = addon_info["version"] folder_name = "{}_{}".format(addon_name, addon_version) addon_dir = os.path.join(addons_dir, folder_name) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 94417a2045..d2a2afbee0 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -13,7 +13,8 @@ Main entrypoints are functions: - get_ayon_project_settings - replacement for 'get_project_settings' - get_ayon_system_settings - replacement for 'get_system_settings' """ - +import os +import collections import json import copy import time @@ -1275,9 +1276,15 @@ def convert_project_settings(ayon_settings, default_settings): class CacheItem: lifetime = 10 - def __init__(self, value): + def __init__(self, value, outdate_time=None): self._value = value - self._outdate_time = time.time() + self.lifetime + if outdate_time is None: + outdate_time = time.time() + self.lifetime + self._outdate_time = outdate_time + + @classmethod + def create_outdated(cls): + return cls({}, 0) def get_value(self): return copy.deepcopy(self._value) @@ -1291,57 +1298,89 @@ class CacheItem: return time.time() > self._outdate_time -class AyonSettingsCache: - _cache_by_project_name = {} - _production_settings = None +class _AyonSettingsCache: + use_bundles = None + variant = None + addon_versions = CacheItem.create_outdated() + studio_settings = CacheItem.create_outdated() + cache_by_project_name = collections.defaultdict( + CacheItem.create_outdated) @classmethod - def get_production_settings(cls): - if ( - cls._production_settings is None - or cls._production_settings.is_outdated - ): + def _use_bundles(cls): + if _AyonSettingsCache.use_bundles is None: + major, minor, _, _, _ = ayon_api.get_server_version_tuple() + _AyonSettingsCache.use_bundles = major == 0 and minor >= 3 + return _AyonSettingsCache.use_bundles + + @classmethod + def _get_variant(cls): + if _AyonSettingsCache.variant is None: from openpype.lib.openpype_version import is_staging_enabled - variant = "staging" if is_staging_enabled() else "production" - value = ayon_api.get_addons_settings( - only_values=False, variant=variant) - if cls._production_settings is None: - cls._production_settings = CacheItem(value) - else: - cls._production_settings.update_value(value) - return cls._production_settings.get_value() + _AyonSettingsCache.variant = ( + "staging" if is_staging_enabled() else "production" + ) + return _AyonSettingsCache.variant + + @classmethod + def _get_bundle_name(cls): + return os.environ["AYON_BUNDLE_NAME"] @classmethod def get_value_by_project(cls, project_name): - production_settings = cls.get_production_settings() - addon_versions = production_settings["versions"] - if project_name is None: - return production_settings["settings"], addon_versions - - cache_item = cls._cache_by_project_name.get(project_name) - if cache_item is None or cache_item.is_outdated: - value = ayon_api.get_addons_settings(project_name) - if cache_item is None: - cache_item = CacheItem(value) - cls._cache_by_project_name[project_name] = cache_item + cache_item = _AyonSettingsCache.cache_by_project_name[project_name] + if cache_item.is_outdated: + if cls._use_bundles(): + value = ayon_api.get_addons_settings( + bundle_name=cls._get_bundle_name(), + project_name=project_name + ) else: - cache_item.update_value(value) + value = ayon_api.get_addons_settings(project_name) + cache_item.update_value(value) + return cache_item.get_value() - return cache_item.get_value(), addon_versions + @classmethod + def _get_addon_versions_from_bundle(cls): + expected_bundle = cls._get_bundle_name() + bundles = ayon_api.get_bundles()["bundles"] + bundle = next( + ( + bundle + for bundle in bundles + if bundle["name"] == expected_bundle + ), + None + ) + if bundle is not None: + return bundle["addons"] + return {} + + @classmethod + def get_addon_versions(cls): + cache_item = _AyonSettingsCache.addon_versions + if cache_item.is_outdated: + if cls._use_bundles(): + addons = cls._get_addon_versions_from_bundle() + else: + settings_data = ayon_api.get_addons_settings( + only_values=False, variant=cls._get_variant()) + addons = settings_data["versions"] + cache_item.update_value(addons) + + return cache_item.get_value() def get_ayon_project_settings(default_values, project_name): - ayon_settings, addon_versions = ( - AyonSettingsCache.get_value_by_project(project_name) - ) + ayon_settings = _AyonSettingsCache.get_value_by_project(project_name) return convert_project_settings(ayon_settings, default_values) def get_ayon_system_settings(default_values): - ayon_settings, addon_versions = ( - AyonSettingsCache.get_value_by_project(None) - ) + addon_versions = _AyonSettingsCache.get_addon_versions() + ayon_settings = _AyonSettingsCache.get_value_by_project(None) + return convert_system_settings( ayon_settings, default_values, addon_versions ) diff --git a/openpype/vendor/python/common/ayon_api/__init__.py b/openpype/vendor/python/common/ayon_api/__init__.py index 9e0738071f..4b4e0f3359 100644 --- a/openpype/vendor/python/common/ayon_api/__init__.py +++ b/openpype/vendor/python/common/ayon_api/__init__.py @@ -1,6 +1,8 @@ +from .version import __version__ from .utils import ( TransferProgress, slugify_string, + create_dependency_package_basename, ) from .server_api import ( ServerAPI, @@ -183,8 +185,11 @@ from ._api import ( __all__ = ( + "__version__", + "TransferProgress", "slugify_string", + "create_dependency_package_basename", "ServerAPI", diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index c702101f2b..c886fed976 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -61,6 +61,7 @@ from .utils import ( entity_data_json_default, failed_json_default, TransferProgress, + create_dependency_package_basename, ) PatternType = type(re.compile("")) @@ -114,6 +115,10 @@ class RestApiResponse(object): self.status = status_code self._data = data + @property + def text(self): + return self._response.text + @property def orig_response(self): return self._response @@ -150,11 +155,13 @@ class RestApiResponse(object): def status_code(self): return self.status - def raise_for_status(self): + def raise_for_status(self, message=None): try: self._response.raise_for_status() except requests.exceptions.HTTPError as exc: - raise HTTPRequestError(str(exc), exc.response) + if message is None: + message = str(exc) + raise HTTPRequestError(message, exc.response) def __enter__(self, *args, **kwargs): return self._response.__enter__(*args, **kwargs) @@ -1303,13 +1310,15 @@ class ServerAPI(object): progress.set_transfer_done() return progress - def _upload_file(self, url, filepath, progress): + def _upload_file(self, url, filepath, progress, request_type=None): + if request_type is None: + request_type = RequestTypes.put kwargs = {} if self._session is None: kwargs["headers"] = self.get_headers() - post_func = self._base_functions_mapping[RequestTypes.post] + post_func = self._base_functions_mapping[request_type] else: - post_func = self._session_functions_mapping[RequestTypes.post] + post_func = self._session_functions_mapping[request_type] with open(filepath, "rb") as stream: stream.seek(0, io.SEEK_END) @@ -1320,7 +1329,9 @@ class ServerAPI(object): response.raise_for_status() progress.set_transferred_size(size) - def upload_file(self, endpoint, filepath, progress=None): + def upload_file( + self, endpoint, filepath, progress=None, request_type=None + ): """Upload file to server. Todos: @@ -1331,6 +1342,8 @@ class ServerAPI(object): filepath (str): Source filepath. progress (Optional[TransferProgress]): Object that gives ability to track upload progress. + request_type (Optional[RequestType]): Type of request that will + be used to upload file. """ if endpoint.startswith(self._base_url): @@ -1349,7 +1362,7 @@ class ServerAPI(object): progress.set_started() try: - self._upload_file(url, filepath, progress) + self._upload_file(url, filepath, progress, request_type) except Exception as exc: progress.set_failed(str(exc)) @@ -1486,13 +1499,11 @@ class ServerAPI(object): """ response = self.delete("attributes/{}".format(attribute_name)) - if response.status_code != 204: - # TODO raise different exception - raise ValueError( - "Attribute \"{}\" was not created/updated. {}".format( - attribute_name, response.detail - ) + response.raise_for_status( + "Attribute \"{}\" was not created/updated. {}".format( + attribute_name, response.detail ) + ) self.reset_attributes_schema() @@ -1732,8 +1743,9 @@ class ServerAPI(object): python_version, platform_name, python_modules, + runtime_python_modules, checksum, - checksum_type, + checksum_algorithm, file_size, sources=None, ): @@ -1742,6 +1754,10 @@ class ServerAPI(object): This step will create only metadata. Make sure to upload installer to the server using 'upload_installer' method. + Runtime python modules are modules that are required to run AYON + desktop application, but are not added to PYTHONPATH for any + subprocess. + Args: filename (str): Installer filename. version (str): Version of installer. @@ -1749,8 +1765,10 @@ class ServerAPI(object): platform_name (str): Name of platform. python_modules (dict[str, str]): Python modules that are available in installer. + runtime_python_modules (dict[str, str]): Runtime python modules + that are available in installer. checksum (str): Installer file checksum. - checksum_type (str): Type of checksum used to create checksum. + checksum_algorithm (str): Type of checksum used to create checksum. file_size (int): File size. sources (Optional[list[dict[str, Any]]]): List of sources that can be used to download file. @@ -1762,8 +1780,9 @@ class ServerAPI(object): "pythonVersion": python_version, "platform": platform_name, "pythonModules": python_modules, + "runtimePythonModules": runtime_python_modules, "checksum": checksum, - "checksumType": checksum_type, + "checksumAlgorithm": checksum_algorithm, "size": file_size, } if sources: @@ -1781,7 +1800,7 @@ class ServerAPI(object): can be used to download file. Fully replaces existing sources. """ - response = self.post( + response = self.patch( "desktop/installers/{}".format(filename), sources=sources ) @@ -1794,7 +1813,7 @@ class ServerAPI(object): filename (str): Installer filename. """ - response = self.delete("dekstop/installers/{}".format(filename)) + response = self.delete("desktop/installers/{}".format(filename)) response.raise_for_status() def download_installer( @@ -1929,8 +1948,7 @@ class ServerAPI(object): checksum=checksum, **kwargs ) - if response.status not in (200, 201, 204): - raise ServerError("Failed to create/update dependency") + response.raise_for_status("Failed to create/update dependency") return response.data def get_dependency_packages(self): @@ -2065,8 +2083,7 @@ class ServerAPI(object): route = self._get_dependency_package_route(filename, platform_name) response = self.delete(route) - if response.status != 200: - raise ServerError("Failed to delete dependency file") + response.raise_for_status("Failed to delete dependency file") return response.data def download_dependency_package( @@ -2131,6 +2148,10 @@ class ServerAPI(object): def create_dependency_package_basename(self, platform_name=None): """Create basename for dependency package file. + Deprecated: + Use 'create_dependency_package_basename' from `ayon_api` or + `ayon_api.utils` instead. + Args: platform_name (Optional[str]): Name of platform for which the bundle is targeted. Default value is current platform. @@ -2139,12 +2160,7 @@ class ServerAPI(object): str: Dependency package name with timestamp and platform. """ - if platform_name is None: - platform_name = platform.system().lower() - - now_date = datetime.datetime.now() - time_stamp = now_date.strftime("%y%m%d%H%M") - return "ayon_{}_{}".format(time_stamp, platform_name) + return create_dependency_package_basename(platform_name) def _get_bundles_route(self): major, minor, patch, _, _ = self.server_version_tuple diff --git a/openpype/vendor/python/common/ayon_api/utils.py b/openpype/vendor/python/common/ayon_api/utils.py index d1f108a220..69fd8e9b41 100644 --- a/openpype/vendor/python/common/ayon_api/utils.py +++ b/openpype/vendor/python/common/ayon_api/utils.py @@ -2,6 +2,7 @@ import re import datetime import uuid import string +import platform import collections try: # Python 3 @@ -449,3 +450,22 @@ class TransferProgress: destination_url = property(get_destination_url, set_destination_url) content_size = property(get_content_size, set_content_size) transferred_size = property(get_transferred_size, set_transferred_size) + + +def create_dependency_package_basename(platform_name=None): + """Create basename for dependency package file. + + Args: + platform_name (Optional[str]): Name of platform for which the + bundle is targeted. Default value is current platform. + + Returns: + str: Dependency package name with timestamp and platform. + """ + + if platform_name is None: + platform_name = platform.system().lower() + + now_date = datetime.datetime.now() + time_stamp = now_date.strftime("%y%m%d%H%M") + return "ayon_{}_{}".format(time_stamp, platform_name) diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index e9dd1f445a..238f6e9426 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.3.1" +__version__ = "0.3.2" From a00fba4b8c7d8bdfa8c6cede525e85a0fb39dab4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 5 Jul 2023 17:56:23 +0200 Subject: [PATCH 256/446] Fix - server returns filename instead of name (#5251) --- common/ayon_common/distribution/data_structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/ayon_common/distribution/data_structures.py b/common/ayon_common/distribution/data_structures.py index 19d3f6c744..a256ef0662 100644 --- a/common/ayon_common/distribution/data_structures.py +++ b/common/ayon_common/distribution/data_structures.py @@ -198,7 +198,7 @@ class DependencyItem(object): def from_dict(cls, package): sources, unknown_sources = prepare_sources(package.get("sources")) return cls( - name=package["name"], + name=package["filename"], platform_name=package["platform"], sources=sources, unknown_sources=unknown_sources, From ba3ccf1e1c406d45be0e8e6ef89d3e5b8a51f769 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Jul 2023 13:57:59 +0200 Subject: [PATCH 257/446] fixed attribute usage od DependecyPackageItem --- common/ayon_common/distribution/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/ayon_common/distribution/control.py b/common/ayon_common/distribution/control.py index 7b38a9a9af..95c221d753 100644 --- a/common/ayon_common/distribution/control.py +++ b/common/ayon_common/distribution/control.py @@ -788,14 +788,14 @@ class AyonDistribution: def _prepare_dependency_progress(self): package = self.dependency_package_item - if package is None or not package.require_distribution: + if package is None: return None metadata = self.get_dependency_metadata() downloader_data = { "type": "dependency_package", "name": package.name, - "platform": package.platform + "platform": package.platform_name } zip_dir = package_dir = os.path.join( self._dependency_dirpath, package.name From 366c8571c5a745c342440ab06284b458831e19c9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Jul 2023 14:24:19 +0200 Subject: [PATCH 258/446] fill filename missing for dependency package sources --- common/ayon_common/distribution/data_structures.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/ayon_common/distribution/data_structures.py b/common/ayon_common/distribution/data_structures.py index a256ef0662..aa93d4ed71 100644 --- a/common/ayon_common/distribution/data_structures.py +++ b/common/ayon_common/distribution/data_structures.py @@ -196,7 +196,11 @@ class DependencyItem(object): @classmethod def from_dict(cls, package): - sources, unknown_sources = prepare_sources(package.get("sources")) + src_sources = package.get("sources") or [] + for source in src_sources: + if source.get("type") == "server" and not source.get("filename"): + source["filename"] = package["filename"] + sources, unknown_sources = prepare_sources(src_sources) return cls( name=package["filename"], platform_name=package["platform"], From 0fce4a4a34b499d971ff7327747825398c665284 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Jul 2023 14:10:15 +0200 Subject: [PATCH 259/446] added pymongo to requirements --- server_addon/client/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/client/pyproject.toml b/server_addon/client/pyproject.toml index 0d49cc7425..6d5ac92ca7 100644 --- a/server_addon/client/pyproject.toml +++ b/server_addon/client/pyproject.toml @@ -12,6 +12,7 @@ shotgun_api3 = {git = "https://github.com/shotgunsoftware/python-api.git", rev = gazu = "^0.9.3" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) jsonschema = "^2.6.0" +pymongo = "^3.11.2" log4mongo = "^1.7" pathlib2= "^2.3.5" # deadline submit publish job only (single place, maybe not needed?) pyblish-base = "^1.8.11" From 3f892c17855dbdf461e55dde6984d02710dd60f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 11 Jul 2023 17:47:09 +0200 Subject: [PATCH 260/446] fix temporary file --- common/ayon_common/connection/credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/ayon_common/connection/credentials.py b/common/ayon_common/connection/credentials.py index ad2ca9a6b2..7f70cb7992 100644 --- a/common/ayon_common/connection/credentials.py +++ b/common/ayon_common/connection/credentials.py @@ -288,7 +288,7 @@ def ask_to_login_ui( "always_on_top": always_on_top, } - with tempfile.TemporaryFile( + with tempfile.NamedTemporaryFile( mode="w", prefix="ayon_login", suffix=".json", delete=False ) as tmp: output = tmp.name From 284b7069ce3ce657f1f500a9d34120ff06df9895 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jul 2023 11:41:34 +0800 Subject: [PATCH 261/446] oscar's comment --- openpype/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 1bb19498da..bbd01f7a4e 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -296,7 +296,7 @@ def deliver_sequence( src_head = src_collection.head src_tail = src_collection.tail uploaded = 0 - first_frame = next(iter(src_collection.indexes)) + first_frame = min(src_collection.indexes) for index in src_collection.indexes: src_padding = src_collection.format("{padding}") % index src_file_name = "{}{}{}".format(src_head, src_padding, src_tail) From 6b87de90ffbb9de3a4fd1e852a8d8c0d519a0087 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 12 Jul 2023 12:41:15 +0300 Subject: [PATCH 262/446] fix type on line 53 --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 614785487f..43b8428c60 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -50,7 +50,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): num_aovs = rop.evalParm("ar_aovs") for index in range(1, num_aovs + 1): # Skip disabled AOVs - if not rop.evalParm("ar_enable_aovP{}".format(index)): + if not rop.evalParm("ar_enable_aov{}".format(index)): continue if rop.evalParm("ar_aov_exr_enable_layer_name{}".format(index)): From ac7b7af3c56b41fba4dbac4581510cb0d80304f3 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 12 Jul 2023 15:54:15 +0300 Subject: [PATCH 263/446] make hound happy --- .../plugins/publish/validate_primitive_hierarchy_paths.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index 7f6198294d..74440a1728 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -150,9 +150,10 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): path_node, path_attr ) - path_node.setGenericFlag(hou.nodeFlag.DisplayComment,True) + path_node.setGenericFlag(hou.nodeFlag.DisplayComment, True) path_node.setComment( - 'Auto path node created automatically by "Add a default path attribute"' + 'Auto path node was created automatically by ' + '"Add a default path attribute"' '\nFeel free to modify or replace it.' ) From 892758b4d02eaf80ea9cc0918a7920d31e0f4961 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 12 Jul 2023 17:05:24 +0200 Subject: [PATCH 264/446] Added missed colorspace injection for image sequences Develop contains this information, was missed during shuffling things around. --- .../plugins/publish/submit_publish_job.py | 7 ++++--- .../publish/create_publish_royalrender_job.py | 7 +++++-- openpype/pipeline/farm/pyblish_functions.py | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index ea9c296732..1bc7ea0161 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -15,7 +15,7 @@ from openpype.client import ( from openpype.pipeline import ( legacy_io, ) -from openpype.pipeline.publish import OpenPypePyblishPluginMixin +from openpype.pipeline import publish from openpype.lib import EnumDef from openpype.tests.lib import is_in_tests from openpype.lib import is_running_from_build @@ -57,7 +57,7 @@ def get_resource_files(resources, frame_range=None): class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, - OpenPypePyblishPluginMixin): + publish.ColormanagedPyblishPluginMixin): """Process Job submitted on farm. These jobs are dependent on a deadline or muster job @@ -396,7 +396,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, anatomy, self.aov_filter, self.skip_integration_repre_list, - do_not_add_review + do_not_add_review, + self ) if "representations" not in instance_skeleton_data.keys(): diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 6eb8f2649e..5f75bbc3f1 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -23,9 +23,11 @@ from openpype.pipeline.farm.pyblish_functions import ( prepare_representations, create_metadata_path ) +from openpype.pipeline import publish -class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin): +class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin, + publish.ColormanagedPyblishPluginMixin): """Creates job which publishes rendered files to publish area. Job waits until all rendering jobs are finished, triggers `publish` command @@ -107,7 +109,8 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin): self.anatomy, self.aov_filter, self.skip_integration_repre_list, - do_not_add_review + do_not_add_review, + self ) if "representations" not in instance_skeleton_data.keys(): diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 0ace02edb9..3834524faa 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -243,7 +243,8 @@ def create_skeleton_instance( "jobBatchName": data.get("jobBatchName", ""), "useSequenceForReview": data.get("useSequenceForReview", True), # map inputVersions `ObjectId` -> `str` so json supports it - "inputVersions": list(map(str, data.get("inputVersions", []))) + "inputVersions": list(map(str, data.get("inputVersions", []))), + "colorspace": data.get("colorspace") } # skip locking version if we are creating v01 @@ -291,7 +292,8 @@ def _add_review_families(families): def prepare_representations(instance, exp_files, anatomy, aov_filter, skip_integration_repre_list, - do_not_add_review): + do_not_add_review, + color_managed_plugin): """Create representations for file sequences. This will return representations of expected files if they are not @@ -306,7 +308,7 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, aov_filter (dict): add review for specific aov names skip_integration_repre_list (list): exclude specific extensions, do_not_add_review (bool): explicitly skip review - + color_managed_plugin (publish.ColormanagedPyblishPluginMixin) Returns: list of representations @@ -433,6 +435,13 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, if not already_there: representations.append(rep) + for rep in representations: + # inject colorspace data + color_managed_plugin.set_representation_colorspace( + rep, instance.context, + colorspace=instance.data["colorspace"] + ) + return representations From 48f06ba414f33f028d7cfbab6eef4fdf261137df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 12 Jul 2023 18:01:50 +0200 Subject: [PATCH 265/446] Fix - removed wrong argument Probably incorrectly merged from develop where it is already fixed. --- openpype/modules/deadline/abstract_submit_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index d9d250fe9e..3fa427204b 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -433,7 +433,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, file_path = None if self.use_published: if not self.import_reference: - file_path = self.from_published_scene(context) + file_path = self.from_published_scene() else: self.log.info("use the scene with imported reference for rendering") # noqa file_path = context.data["currentFile"] From abc6a1eb27d0e574c9f6d934d25f67ad7be681db Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 12 Jul 2023 18:06:17 +0200 Subject: [PATCH 266/446] Fix - context must be passe to set_representation_colorspace --- .../plugins/publish/submit_publish_job.py | 2 ++ .../publish/create_publish_royalrender_job.py | 1 + openpype/pipeline/farm/pyblish_functions.py | 35 ++++++++++--------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 9e3ad20b44..457ebfd0fe 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -57,6 +57,7 @@ def get_resource_files(resources, frame_range=None): class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, + publish.OpenPypePyblishPluginMixin, publish.ColormanagedPyblishPluginMixin): """Process Job submitted on farm. @@ -396,6 +397,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, self.aov_filter, self.skip_integration_repre_list, do_not_add_review, + instance.context, self ) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 5f75bbc3f1..3eb49a39ee 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -110,6 +110,7 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin, self.aov_filter, self.skip_integration_repre_list, do_not_add_review, + instance.context, self ) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 76ee5832d6..2df8269d79 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -290,9 +290,10 @@ def _add_review_families(families): return families -def prepare_representations(instance, exp_files, anatomy, aov_filter, +def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter, skip_integration_repre_list, do_not_add_review, + context, color_managed_plugin): """Create representations for file sequences. @@ -301,7 +302,7 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, most cases, but if not - we create representation from each of them. Arguments: - instance (dict): instance data for which we are + skeleton_data (dict): instance data for which we are setting representations exp_files (list): list of expected files anatomy (Anatomy): @@ -328,9 +329,9 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, # expected files contains more explicitly and from what # should be review made. # - "review" tag is never added when is set to 'False' - if instance["useSequenceForReview"]: + if skeleton_data["useSequenceForReview"]: # toggle preview on if multipart is on - if instance.get("multipartExr", False): + if skeleton_data.get("multipartExr", False): log.debug( "Adding preview tag because its multipartExr" ) @@ -355,8 +356,8 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, " This may cause issues on farm." ).format(staging)) - frame_start = int(instance.get("frameStartHandle")) - if instance.get("slate"): + frame_start = int(skeleton_data.get("frameStartHandle")) + if skeleton_data.get("slate"): frame_start -= 1 # explicitly disable review by user @@ -366,10 +367,10 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, "ext": ext, "files": [os.path.basename(f) for f in list(collection)], "frameStart": frame_start, - "frameEnd": int(instance.get("frameEndHandle")), + "frameEnd": int(skeleton_data.get("frameEndHandle")), # If expectedFile are absolute, we need only filenames "stagingDir": staging, - "fps": instance.get("fps"), + "fps": skeleton_data.get("fps"), "tags": ["review"] if preview else [], } @@ -377,18 +378,19 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, if ext in skip_integration_repre_list: rep["tags"].append("delete") - if instance.get("multipartExr", False): + if skeleton_data.get("multipartExr", False): rep["tags"].append("multipartExr") # support conversion from tiled to scanline - if instance.get("convertToScanline"): + if skeleton_data.get("convertToScanline"): log.info("Adding scanline conversion.") rep["tags"].append("toScanline") representations.append(rep) if preview: - instance["families"] = _add_review_families(instance["families"]) + skeleton_data["families"] = _add_review_families( + skeleton_data["families"]) # add remainders as representations for remainder in remainders: @@ -419,13 +421,14 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, preview = preview and not do_not_add_review if preview: rep.update({ - "fps": instance.get("fps"), + "fps": skeleton_data.get("fps"), "tags": ["review"] }) - instance["families"] = _add_review_families(instance["families"]) + skeleton_data["families"] = \ + _add_review_families(skeleton_data["families"]) already_there = False - for repre in instance.get("representations", []): + for repre in skeleton_data.get("representations", []): # might be added explicitly before by publish_on_farm already_there = repre.get("files") == rep["files"] if already_there: @@ -438,8 +441,8 @@ def prepare_representations(instance, exp_files, anatomy, aov_filter, for rep in representations: # inject colorspace data color_managed_plugin.set_representation_colorspace( - rep, instance.context, - colorspace=instance.data["colorspace"] + rep, context, + colorspace=skeleton_data["colorspace"] ) return representations From 94c53c40c1b8e28f602331eaa9dd1c3853ad55a5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jul 2023 11:16:10 +0200 Subject: [PATCH 267/446] nuke: removing deprecated settings connection --- openpype/hosts/nuke/api/plugin.py | 36 +------------------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index f7bbea3d01..cfdb407d26 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -824,41 +824,6 @@ class ExporterReviewMov(ExporterReview): add_tags = [] self.publish_on_farm = farm read_raw = kwargs["read_raw"] - - # TODO: remove this when `reformat_nodes_config` - # is changed in settings - reformat_node_add = kwargs["reformat_node_add"] - reformat_node_config = kwargs["reformat_node_config"] - - # TODO: make this required in future - reformat_nodes_config = kwargs.get("reformat_nodes_config", {}) - - # TODO: remove this once deprecated is removed - # make sure only reformat_nodes_config is used in future - if reformat_node_add and reformat_nodes_config.get("enabled"): - self.log.warning( - "`reformat_node_add` is deprecated. " - "Please use only `reformat_nodes_config` instead.") - reformat_nodes_config = None - - # TODO: reformat code when backward compatibility is not needed - # warning if reformat_nodes_config is not set - if not reformat_nodes_config: - self.log.warning( - "Please set `reformat_nodes_config` in settings. " - "Using `reformat_node_config` instead." - ) - reformat_nodes_config = { - "enabled": reformat_node_add, - "reposition_nodes": [ - { - "node_class": "Reformat", - "knobs": reformat_node_config - } - ] - } - - bake_viewer_process = kwargs["bake_viewer_process"] bake_viewer_input_process_node = kwargs[ "bake_viewer_input_process"] @@ -897,6 +862,7 @@ class ExporterReviewMov(ExporterReview): self._shift_to_previous_node_and_temp(subset, r_node, "Read... `{}`") # add reformat node + reformat_nodes_config = kwargs["reformat_nodes_config"] if reformat_nodes_config["enabled"]: reposition_nodes = reformat_nodes_config["reposition_nodes"] for reposition_node in reposition_nodes: From 46a238c8b6692a7b21c3c2d8e22198aa57a55c31 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 13 Jul 2023 14:20:27 +0200 Subject: [PATCH 268/446] Slack - enhanced logging and protection against failure (#5287) * OP-6248 - enhanced logging and protection against failure Covered issues found in production on customer site. SlackAPI exception doesn't need to have 'error', covered uncaught exception. --------- Co-authored-by: Roy Nieterau --- .../plugins/publish/integrate_slack_api.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 86c97586d2..4c5a39318a 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -350,6 +350,10 @@ class SlackPython3Operations(AbstractSlackOperations): self.log.warning("Cannot pull user info, " "mentions won't work", exc_info=True) return [], [] + except Exception: + self.log.warning("Cannot pull user info, " + "mentions won't work", exc_info=True) + return [], [] return users, groups @@ -377,8 +381,12 @@ class SlackPython3Operations(AbstractSlackOperations): return response.data["ts"], file_ids except SlackApiError as e: # # You will get a SlackApiError if "ok" is False - error_str = self._enrich_error(str(e.response["error"]), channel) - self.log.warning("Error happened {}".format(error_str)) + if e.response.get("error"): + error_str = self._enrich_error(str(e.response["error"]), channel) + else: + error_str = self._enrich_error(str(e), channel) + self.log.warning("Error happened: {}".format(error_str), + exc_info=True) except Exception as e: error_str = self._enrich_error(str(e), channel) self.log.warning("Not SlackAPI error", exc_info=True) @@ -448,12 +456,14 @@ class SlackPython2Operations(AbstractSlackOperations): if response.get("error"): error_str = self._enrich_error(str(response.get("error")), channel) - self.log.warning("Error happened: {}".format(error_str)) + self.log.warning("Error happened: {}".format(error_str), + exc_info=True) else: return response["ts"], file_ids except Exception as e: # You will get a SlackApiError if "ok" is False error_str = self._enrich_error(str(e), channel) - self.log.warning("Error happened: {}".format(error_str)) + self.log.warning("Error happened: {}".format(error_str), + exc_info=True) return None, [] From 3d21df1fc5e24e4009d949f5c7172c26660fb5eb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 13 Jul 2023 13:41:17 +0100 Subject: [PATCH 269/446] Avoid remove and update to delete new data This commit avoids that when updating or removing an asset, all new data, such as animations (actions), are removed. --- .../hosts/blender/plugins/load/load_blend.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 53933a1934..a0ccd65f29 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -23,7 +23,7 @@ class BlendLoader(plugin.AssetLoader): families = ["model", "rig", "layout", "camera"] representations = ["blend"] - label = "Load Blend" + label = "Append Blend" icon = "code-fork" color = "orange" @@ -70,10 +70,13 @@ class BlendLoader(plugin.AssetLoader): for attr in dir(data_to): setattr(data_to, attr, getattr(data_from, attr)) + members = [] + # Rename the object to add the asset name for attr in dir(data_to): for data in getattr(data_to, attr): data.name = f"{group_name}:{data.name}" + members.append(data) container = self._get_asset_container(data_to.objects) assert container, "No asset group found" @@ -92,7 +95,7 @@ class BlendLoader(plugin.AssetLoader): library = bpy.data.libraries.get(bpy.path.basename(libpath)) bpy.data.libraries.remove(library) - return container + return container, members def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -126,7 +129,7 @@ class BlendLoader(plugin.AssetLoader): avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) bpy.context.scene.collection.children.link(avalon_container) - container = self._process_data(libpath, group_name) + container, members = self._process_data(libpath, group_name) if family == "layout": self._post_process_layout(container, asset, representation) @@ -144,7 +147,8 @@ class BlendLoader(plugin.AssetLoader): "asset_name": asset_name, "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"], - "objectName": group_name + "objectName": group_name, + "members": members, } container[AVALON_PROPERTY] = data @@ -175,7 +179,7 @@ class BlendLoader(plugin.AssetLoader): self.exec_remove(container) - asset_group = self._process_data(libpath, group_name) + asset_group, members = self._process_data(libpath, group_name) avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.objects.link(asset_group) @@ -183,12 +187,17 @@ class BlendLoader(plugin.AssetLoader): asset_group.matrix_basis = transform asset_group.parent = parent + # Restore the old data, but reset memebers, as they don't exist anymore + # This avoids a crash, because the memory addresses of those members + # are not valid anymore + old_data["members"] = [] asset_group[AVALON_PROPERTY] = old_data new_data = { "libpath": libpath, "representation": str(representation["_id"]), "parent": str(representation["parent"]), + "members": members, } imprint(asset_group, new_data) @@ -210,7 +219,10 @@ class BlendLoader(plugin.AssetLoader): for attr in attrs: for data in getattr(bpy.data, attr): - if data.name.startswith(f"{group_name}:"): + if data in asset_group.get(AVALON_PROPERTY).get("members", []): + # Skip the asset group + if data == asset_group: + continue getattr(bpy.data, attr).remove(data) bpy.data.objects.remove(asset_group) From 5b5e64b586c4c1b9247c9396c17ce4c95352f054 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 13 Jul 2023 14:44:51 +0200 Subject: [PATCH 270/446] Removed unnecessary import of pyblish.cli (#5292) This import resulted in adding additional logging handler which lead to duplication of logs in hosts with plugins containing `is_in_tests` method. Import is unnecessary for testing functionality. --- openpype/tests/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tests/lib.py b/openpype/tests/lib.py index 1fa5fb8054..c7d4423aba 100644 --- a/openpype/tests/lib.py +++ b/openpype/tests/lib.py @@ -5,7 +5,6 @@ import tempfile import contextlib import pyblish -import pyblish.cli import pyblish.plugin from pyblish.vendor import six From f725992818c45c8f38206dff8cb21fd99310551c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 13 Jul 2023 12:47:41 +0000 Subject: [PATCH 271/446] [Automated] Release --- CHANGELOG.md | 848 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 850 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b74fea7e3d..33dbdb14fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,854 @@ # Changelog +## [3.16.0](https://github.com/ynput/OpenPype/tree/3.16.0) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/...3.16.0) + +### **🆕 New features** + + +