From 9fe4b635174060697a9ea8a9e33f47394a34d9e9 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 12:56:24 +0200 Subject: [PATCH 001/155] refactor avalon imports from lib_template_builder --- .../hosts/maya/api/lib_template_builder.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 openpype/hosts/maya/api/lib_template_builder.py diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py new file mode 100644 index 0000000000..172a6f9b2b --- /dev/null +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -0,0 +1,184 @@ +from collections import OrderedDict +import maya.cmds as cmds + +import qargparse +from openpype.tools.utils.widgets import OptionDialog +from lib import get_main_window, imprint + +# To change as enum +build_types = ["context_asset", "linked_asset", "all_assets"] + + +def get_placeholder_attributes(node): + return { + attr: cmds.getAttr("{}.{}".format(node, attr)) + for attr in cmds.listAttr(node, userDefined=True)} + + +def delete_placeholder_attributes(node): + ''' + function to delete all extra placeholder attributes + ''' + extra_attributes = get_placeholder_attributes(node) + for attribute in extra_attributes: + cmds.deleteAttr(node + '.' + attribute) + + +def create_placeholder(): + args = placeholder_window() + + if not args: + return # operation canceled, no locator created + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] + if selection: + cmds.parent(placeholder, selection[0]) + # custom arg parse to force empty data query + # and still imprint them on placeholder + # and getting items when arg is of type Enumerator + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + # Some tweaks because imprint force enums to to default value so we get + # back arg read and force them to attributes + imprint_enum(placeholder, args) + + # Add helper attributes to keep placeholder info + cmds.addAttr( + placeholder, longName="parent", + hidden=True, dataType="string") + cmds.addAttr( + placeholder, longName="index", + hidden=True, attributeType="short", + defaultValue=-1) + + +def update_placeholder(): + placeholder = cmds.ls(selection=True) + if len(placeholder) == 0: + raise ValueError("No node selected") + if len(placeholder) > 1: + raise ValueError("Too many selected nodes") + placeholder = placeholder[0] + + args = placeholder_window(get_placeholder_attributes(placeholder)) + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + if not args: + return # operation canceled + + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + imprint_enum(placeholder, args) + + +def imprint_enum(placeholder, args): + """ + Imprint method doesn't act properly with enums. + Replacing the functionnality with this for now + """ + enum_values = {str(arg): arg.read() + for arg in args if arg._data.get("items")} + string_to_value_enum_table = { + build: i for i, build + in enumerate(build_types)} + for key, value in enum_values.items(): + cmds.setAttr( + placeholder + "." + key, + string_to_value_enum_table[value]) + + +def placeholder_window(options=None): + options = options or dict() + dialog = OptionDialog(parent=get_main_window()) + dialog.setWindowTitle("Create Placeholder") + + args = [ + qargparse.Separator("Main attributes"), + qargparse.Enum( + "builder_type", + label="Asset Builder Type", + default=options.get("builder_type", 0), + items=build_types, + help="""Asset Builder Type +Builder type describe what template loader will look for. +context_asset : Template loader will look for subsets of +current context asset (Asset bob will find asset) +linked_asset : Template loader will look for assets linked +to current context asset. +Linked asset are looked in avalon database under field "inputLinks" +""" + ), + qargparse.String( + "family", + default=options.get("family", ""), + label="OpenPype Family", + placeholder="ex: model, look ..."), + qargparse.String( + "representation", + default=options.get("representation", ""), + label="OpenPype Representation", + placeholder="ex: ma, abc ..."), + qargparse.String( + "loader", + default=options.get("loader", ""), + label="Loader", + placeholder="ex: ReferenceLoader, LightLoader ...", + help="""Loader +Defines what openpype loader will be used to load assets. +Useable loader depends on current host's loader list. +Field is case sensitive. +"""), + qargparse.String( + "loader_args", + default=options.get("loader_args", ""), + label="Loader Arguments", + placeholder='ex: {"camera":"persp", "lights":True}', + help="""Loader +Defines a dictionnary of arguments used to load assets. +Useable arguments depend on current placeholder Loader. +Field should be a valid python dict. Anything else will be ignored. +"""), + qargparse.Integer( + "order", + default=options.get("order", 0), + min=0, + max=999, + label="Order", + placeholder="ex: 0, 100 ... (smallest order loaded first)", + help="""Order +Order defines asset loading priority (0 to 999) +Priority rule is : "lowest is first to load"."""), + qargparse.Separator( + "Optional attributes"), + qargparse.String( + "asset", + default=options.get("asset", ""), + label="Asset filter", + placeholder="regex filtering by asset name", + help="Filtering assets by matching field regex to asset's name"), + qargparse.String( + "subset", + default=options.get("subset", ""), + label="Subset filter", + placeholder="regex filtering by subset name", + help="Filtering assets by matching field regex to subset's name"), + qargparse.String( + "hierarchy", + default=options.get("hierarchy", ""), + label="Hierarchy filter", + placeholder="regex filtering by asset's hierarchy", + help="Filtering assets by matching field asset's hierarchy") + ] + dialog.create(args) + + if not dialog.exec_(): + return None + + return args From 69a388de1319eb49de84a0f6d846631623fc5a7d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 14:58:27 +0200 Subject: [PATCH 002/155] add the templated wrokfile build schema for maya --- .../defaults/project_settings/maya.json | 8 +++++ .../projects_schema/schema_project_maya.json | 4 +++ .../schema_templated_workfile_build.json | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index a42f889e85..303cd052bb 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -718,6 +718,14 @@ } ] }, + "templated_workfile_build": { + "profiles": [ + { + "task_types": [], + "path": "/path/to/your/template" + } + ] + }, "filters": { "preset 1": { "ValidateNoAnimation": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 40e98b0333..d137049e9e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -73,6 +73,10 @@ "type": "schema", "name": "schema_workfile_build" }, + { + "type": "schema", + "name": "schema_templated_workfile_build" + }, { "type": "schema", "name": "schema_publish_gui_filter" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json new file mode 100644 index 0000000000..01e74f64b0 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "path", + "label": "Path to template", + "type": "text", + "object_type": "text" + } + ] + } + } + ] +} From 108597f9b1e139f31e6b0f20568866cb2971020a Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 17:28:42 +0200 Subject: [PATCH 003/155] add placeholder menu to maya --- .../hosts/maya/api/lib_template_builder.py | 2 +- openpype/hosts/maya/api/menu.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 172a6f9b2b..d8772f3f9a 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -3,7 +3,7 @@ import maya.cmds as cmds import qargparse from openpype.tools.utils.widgets import OptionDialog -from lib import get_main_window, imprint +from .lib import get_main_window, imprint # To change as enum build_types = ["context_asset", "linked_asset", "all_assets"] diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 97f06c43af..8beaf491bb 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -11,8 +11,10 @@ from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib + from .lib import get_main_window, IS_HEADLESS from .commands import reset_frame_range +from .lib_template_builder import create_placeholder, update_placeholder log = logging.getLogger(__name__) @@ -139,6 +141,24 @@ def install(): parent_widget ) ) + + builder_menu = cmds.menuItem( + "Template Builder", + subMenu=True, + tearOff=True, + parent=MENU_NAME + ) + cmds.menuItem( + "Create Placeholder", + parent=builder_menu, + command=lambda *args: create_placeholder() + ) + cmds.menuItem( + "Update Placeholder", + parent=builder_menu, + command=lambda *args: update_placeholder() + ) + cmds.setParent(MENU_NAME, menu=True) def add_scripts_menu(): From 199aba87727d7a2417d7f8122dd34f6e4160b467 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:25:39 +0200 Subject: [PATCH 004/155] setup build template in openpype lib --- openpype/lib/__init__.py | 2 + openpype/lib/abstract_template_loader.py | 447 ++++++++++++++++++++++ openpype/lib/avalon_context.py | 222 +++++------ openpype/lib/build_template.py | 61 +++ openpype/lib/build_template_exceptions.py | 35 ++ 5 files changed, 660 insertions(+), 107 deletions(-) create mode 100644 openpype/lib/abstract_template_loader.py create mode 100644 openpype/lib/build_template.py create mode 100644 openpype/lib/build_template_exceptions.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 8d4e733b7d..8f3919d378 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -136,6 +136,7 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, + get_loaders_by_name, BuildWorkfile, @@ -308,6 +309,7 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", + "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py new file mode 100644 index 0000000000..6888cbf757 --- /dev/null +++ b/openpype/lib/abstract_template_loader.py @@ -0,0 +1,447 @@ +import os +from abc import ABCMeta, abstractmethod + +import traceback + +import six + +import openpype +from openpype.settings import get_project_settings +from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name +from openpype.api import PypeLogger as Logger +from openpype.pipeline import legacy_io + +from functools import reduce + +from openpype.lib.build_template_exceptions import ( + TemplateAlreadyImported, + TemplateLoadingFailed, + TemplateProfileNotFound, + TemplateNotFound +) + + +def update_representations(entities, entity): + if entity['context']['subset'] not in entities: + entities[entity['context']['subset']] = entity + else: + current = entities[entity['context']['subset']] + incomming = entity + entities[entity['context']['subset']] = max( + current, incomming, + key=lambda entity: entity["context"].get("version", -1)) + + return entities + + +def parse_loader_args(loader_args): + if not loader_args: + return dict() + try: + parsed_args = eval(loader_args) + if not isinstance(parsed_args, dict): + return dict() + else: + return parsed_args + except Exception as err: + print( + "Error while parsing loader arguments '{}'.\n{}: {}\n\n" + "Continuing with default arguments. . .".format( + loader_args, + err.__class__.__name__, + err)) + return dict() + + +@six.add_metaclass(ABCMeta) +class AbstractTemplateLoader: + """ + Abstraction of Template Loader. + Properties: + template_path : property to get current template path + Methods: + import_template : Abstract Method. Used to load template, + depending on current host + get_template_nodes : Abstract Method. Used to query nodes acting + as placeholders. Depending on current host + """ + + def __init__(self, placeholder_class): + + self.loaders_by_name = get_loaders_by_name() + self.current_asset = legacy_io.Session["AVALON_ASSET"] + self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.host_name = legacy_io.Session["AVALON_APP"] + self.task_name = legacy_io.Session["AVALON_TASK"] + self.placeholder_class = placeholder_class + self.current_asset_docs = legacy_io.find_one({ + "type": "asset", + "name": self.current_asset + }) + self.task_type = ( + self.current_asset_docs + .get("data", {}) + .get("tasks", {}) + .get(self.task_name, {}) + .get("type") + ) + + self.log = Logger().get_logger("BUILD TEMPLATE") + + self.log.info( + "BUILDING ASSET FROM TEMPLATE :\n" + "Starting templated build for {asset} in {project}\n\n" + "Asset : {asset}\n" + "Task : {task_name} ({task_type})\n" + "Host : {host}\n" + "Project : {project}\n".format( + asset=self.current_asset, + host=self.host_name, + project=self.project_name, + task_name=self.task_name, + task_type=self.task_type + )) + # Skip if there is no loader + if not self.loaders_by_name: + self.log.warning( + "There is no registered loaders. No assets will be loaded") + return + + def template_already_imported(self, err_msg): + """In case template was already loaded. + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case.""" + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateAlreadyImported(err_msg) + + def template_loading_failed(self, err_msg): + """In case template loading failed + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case. + """ + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateLoadingFailed(err_msg) + + @property + def template_path(self): + """ + Property returning template path. Avoiding setter. + Getting template path from open pype settings based on current avalon + session and solving the path variables if needed. + Returns: + str: Solved template path + Raises: + TemplateProfileNotFound: No profile found from settings for + current avalon session + KeyError: Could not solve path because a key does not exists + in avalon context + TemplateNotFound: Solved path does not exists on current filesystem + """ + project_name = self.project_name + host_name = self.host_name + task_name = self.task_name + task_type = self.task_type + + anatomy = Anatomy(project_name) + project_settings = get_project_settings(project_name) + + build_info = project_settings[host_name]['templated_workfile_build'] + profiles = build_info['profiles'] + + for prf in profiles: + if prf['task_types'] and task_type not in prf['task_types']: + continue + if prf['task_names'] and task_name not in prf['task_names']: + continue + path = prf['path'] + break + else: # IF no template were found (no break happened) + raise TemplateProfileNotFound( + "No matching profile found for task '{}' of type '{}' " + "with host '{}'".format(task_name, task_type, host_name) + ) + if path is None: + raise TemplateLoadingFailed( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles".format(host_name.title())) + try: + solved_path = None + while True: + solved_path = anatomy.path_remapper(path) + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + finally: + solved_path = os.path.normpath(solved_path) + + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in openPype settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + self.log.info("Found template at : '{}'".format(solved_path)) + + return solved_path + + def populate_template(self, ignored_ids=None): + """ + Use template placeholders to load assets and parent them in hierarchy + Arguments : + ignored_ids : + Returns: + None + """ + loaders_by_name = self.loaders_by_name + current_asset = self.current_asset + linked_assets = [asset['name'] for asset + in get_linked_assets(self.current_asset_docs)] + + ignored_ids = ignored_ids or [] + placeholders = self.get_placeholders() + for placeholder in placeholders: + placeholder_representations = self.get_placeholder_representations( + placeholder, + current_asset, + linked_assets + ) + for representation in placeholder_representations: + + self.preload(placeholder, loaders_by_name, representation) + + if self.load_data_is_incorrect( + placeholder, + representation, + ignored_ids): + continue + + self.log.info( + "Loading {}_{} with loader {}\n" + "Loader arguments used : {}".format( + representation['context']['asset'], + representation['context']['subset'], + placeholder.loader, + placeholder.data['loader_args'])) + + try: + container = self.load( + placeholder, loaders_by_name, representation) + except Exception: + self.load_failed(placeholder, representation) + else: + self.load_succeed(placeholder, container) + finally: + self.postload(placeholder) + + def get_placeholder_representations( + self, placeholder, current_asset, linked_assets): + placeholder_db_filters = placeholder.convert_to_db_filters( + current_asset, + linked_assets) + # get representation by assets + for db_filter in placeholder_db_filters: + placeholder_representations = list(avalon.io.find(db_filter)) + for representation in reduce(update_representations, + placeholder_representations, + dict()).values(): + yield representation + + def load_data_is_incorrect( + self, placeholder, last_representation, ignored_ids): + if not last_representation: + self.log.warning(placeholder.err_message()) + return True + if (str(last_representation['_id']) in ignored_ids): + print("Ignoring : ", last_representation['_id']) + return True + return False + + def preload(self, placeholder, loaders_by_name, last_representation): + pass + + def load(self, placeholder, loaders_by_name, last_representation): + return openpype.pipeline.load( + loaders_by_name[placeholder.loader], + last_representation['_id'], + options=parse_loader_args(placeholder.data['loader_args'])) + + def load_succeed(self, placeholder, container): + placeholder.parent_in_hierarchy(container) + + def load_failed(self, placeholder, last_representation): + self.log.warning("Got error trying to load {}:{} with {}\n\n" + "{}".format(last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader, + traceback.format_exc())) + + def postload(self, placeholder): + placeholder.clean() + + def update_missing_containers(self): + loaded_containers_ids = self.get_loaded_containers_by_id() + self.populate_template(ignored_ids=loaded_containers_ids) + + def get_placeholders(self): + placeholder_class = self.placeholder_class + placeholders = map(placeholder_class, self.get_template_nodes()) + valid_placeholders = filter(placeholder_class.is_valid, placeholders) + sorted_placeholders = sorted(valid_placeholders, + key=placeholder_class.order) + return sorted_placeholders + + @abstractmethod + def get_loaded_containers_by_id(self): + """ + Collect already loaded containers for updating scene + Return: + dict (string, node): A dictionnary id as key + and containers as value + """ + pass + + @abstractmethod + def import_template(self, template_path): + """ + Import template in current host + Args: + template_path (str): fullpath to current task and + host's template file + Return: + None + """ + pass + + @abstractmethod + def get_template_nodes(self): + """ + Returning a list of nodes acting as host placeholders for + templating. The data representation is by user. + AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes + Args : + None + Returns: + list(AnyNode): Solved template path + """ + pass + + +@six.add_metaclass(ABCMeta) +class AbstractPlaceholder: + """Abstraction of placeholders logic + Properties: + attributes: A list of mandatory attribute to decribe placeholder + and assets to load. + optional_attributes: A list of optional attribute to decribe + placeholder and assets to load + loader: Name of linked loader to use while loading assets + is_context: Is placeholder linked + to context asset (or to linked assets) + Methods: + is_repres_valid: + loader: + order: + is_valid: + get_data: + parent_in_hierachy: + """ + + attributes = {'builder_type', 'op_family', 'op_representation', + 'order', 'loader', 'loader_args'} + optional_attributes = {} + + def __init__(self, node): + self.get_data(node) + + def order(self): + """Get placeholder order. + Order is used to sort them by priority + Priority is lowset first, highest last + (ex: + 1: First to load + 100: Last to load) + Returns: + Int: Order priority + """ + return self.data.get('order') + + @property + def loader(self): + """Return placeholder loader type + Returns: + string: Loader name + """ + return self.data.get('loader') + + @property + def is_context(self): + """Return placeholder type + context_asset: For loading current asset + linked_asset: For loading linked assets + Returns: + bool: true if placeholder is a context placeholder + """ + return self.data.get('builder_type') == 'context_asset' + + def is_valid(self): + """Test validity of placeholder + i.e.: every attributes exists in placeholder data + Returns: + Bool: True if every attributes are a key of data + """ + if set(self.attributes).issubset(self.data.keys()): + print("Valid placeholder : {}".format(self.data["node"])) + return True + print("Placeholder is not valid : {}".format(self.data["node"])) + return False + + @abstractmethod + def parent_in_hierarchy(self, containers): + """Place container in correct hierarchy + given by placeholder + Args: + containers (String): Container name returned back by + placeholder's loader. + """ + pass + + @abstractmethod + def clean(self): + """Clean placeholder from hierarchy after loading assets. + """ + pass + + @abstractmethod + def convert_to_db_filters(self, current_asset, linked_asset): + """map current placeholder data as a db filter + args: + current_asset (String): Name of current asset in context + linked asset (list[String]) : Names of assets linked to + current asset in context + Returns: + dict: a dictionnary describing a filter to look for asset in + a database + """ + pass + + @abstractmethod + def get_data(self, node): + """ + Collect placeholders information. + Args: + node (AnyNode): A unique node decided by Placeholder implementation + """ + pass diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 9d8a92cfe9..8c80b4a4ae 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,6 +15,7 @@ from openpype.settings import ( get_project_settings, get_system_settings ) + from .anatomy import Anatomy from .profiles_filtering import filter_profiles from .events import emit_event @@ -922,6 +923,118 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): ) +@with_pipeline_io +def collect_last_version_repres(asset_entities): + """Collect subsets, versions and representations for asset_entities. + + Args: + asset_entities (list): Asset entities for which want to find data + + Returns: + (dict): collected entities + + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... + } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(legacy_io.find({ + "type": "subset", + "parent": {"$in": list(asset_entity_by_ids.keys())} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(legacy_io.find({ + "type": "version", + "parent": {"$in": list(subset_entity_by_ids.keys())} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = legacy_io.find({ + "type": "representation", + "parent": {"$in": list(last_versions_by_id.keys())} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output + + +@with_pipeline_io +def get_loaders_by_name(): + from openpype.pipeline import discover_loader_plugins + + loaders_by_name = {} + for loader in discover_loader_plugins(): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + class BuildWorkfile: """Wrapper for build workfile process. @@ -979,8 +1092,6 @@ class BuildWorkfile: ... }] """ - from openpype.pipeline import discover_loader_plugins - # Get current asset name and entity current_asset_name = legacy_io.Session["AVALON_ASSET"] current_asset_entity = legacy_io.find_one({ @@ -996,14 +1107,7 @@ class BuildWorkfile: return # Prepare available loaders - loaders_by_name = {} - for loader in discover_loader_plugins(): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError( - "Duplicated loader name {0}!".format(loader_name) - ) - loaders_by_name[loader_name] = loader + loaders_by_name = get_loaders_by_name() # Skip if there are any loaders if not loaders_by_name: @@ -1075,7 +1179,7 @@ class BuildWorkfile: return # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(assets) + prepared_entities = collect_last_version_repres(assets) # Load containers by prepared entities and presets loaded_containers = [] @@ -1491,102 +1595,6 @@ class BuildWorkfile: return loaded_containers - @with_pipeline_io - def _collect_last_version_repres(self, asset_entities): - """Collect subsets, versions and representations for asset_entities. - - Args: - asset_entities (list): Asset entities for which want to find data - - Returns: - (dict): collected entities - - Example output: - ``` - { - {Asset ID}: { - "asset_entity": , - "subsets": { - {Subset ID}: { - "subset_entity": , - "version": { - "version_entity": , - "repres": [ - , , ... - ] - } - }, - ... - } - }, - ... - } - output[asset_id]["subsets"][subset_id]["version"]["repres"] - ``` - """ - - if not asset_entities: - return {} - - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) - - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version - - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) - - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, - "version": { - "version_entity": version, - "repres": [] - } - } - - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre - ) - - return output - @with_pipeline_io def get_creator_by_name(creator_name, case_sensitive=False): diff --git a/openpype/lib/build_template.py b/openpype/lib/build_template.py new file mode 100644 index 0000000000..7f749cbec2 --- /dev/null +++ b/openpype/lib/build_template.py @@ -0,0 +1,61 @@ +from openpype.pipeline import registered_host +from openpype.lib import classes_from_module +from importlib import import_module + +from .abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader) + +from .build_template_exceptions import ( + TemplateLoadingFailed, + TemplateAlreadyImported, + MissingHostTemplateModule, + MissingTemplatePlaceholderClass, + MissingTemplateLoaderClass +) + +_module_path_format = 'openpype.{host}.template_loader' + + +def build_workfile_template(*args): + template_loader = build_template_loader() + try: + template_loader.import_template(template_loader.template_path) + except TemplateAlreadyImported as err: + template_loader.template_already_imported(err) + except TemplateLoadingFailed as err: + template_loader.template_loading_failed(err) + else: + template_loader.populate_template() + + +def update_workfile_template(args): + template_loader = build_template_loader() + template_loader.update_missing_containers() + + +def build_template_loader(): + host_name = registered_host().__name__.partition('.')[2] + module_path = _module_path_format.format(host=host_name) + module = import_module(module_path) + if not module: + raise MissingHostTemplateModule( + "No template loader found for host {}".format(host_name)) + + template_loader_class = classes_from_module( + AbstractTemplateLoader, + module + ) + template_placeholder_class = classes_from_module( + AbstractPlaceholder, + module + ) + + if not template_loader_class: + raise MissingTemplateLoaderClass() + template_loader_class = template_loader_class[0] + + if not template_placeholder_class: + raise MissingTemplatePlaceholderClass() + template_placeholder_class = template_placeholder_class[0] + return template_loader_class(template_placeholder_class) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py new file mode 100644 index 0000000000..d781eff204 --- /dev/null +++ b/openpype/lib/build_template_exceptions.py @@ -0,0 +1,35 @@ +class MissingHostTemplateModule(Exception): + """Error raised when expected module does not exists""" + pass + + +class MissingTemplatePlaceholderClass(Exception): + """Error raised when module doesn't implement a placeholder class""" + pass + + +class MissingTemplateLoaderClass(Exception): + """Error raised when module doesn't implement a template loader class""" + pass + + +class TemplateNotFound(Exception): + """Exception raised when template does not exist.""" + pass + + +class TemplateProfileNotFound(Exception): + """Exception raised when current profile + doesn't match any template profile""" + pass + + +class TemplateAlreadyImported(Exception): + """Error raised when Template was already imported by host for + this session""" + pass + + +class TemplateLoadingFailed(Exception): + """Error raised whend Template loader was unable to load the template""" + pass \ No newline at end of file From bd884262b0c001715432f28ec1cae6feeeabfed1 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:52:44 +0200 Subject: [PATCH 005/155] add template loader module --- openpype/hosts/maya/api/template_loader.py | 242 +++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 openpype/hosts/maya/api/template_loader.py diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py new file mode 100644 index 0000000000..0e346ca411 --- /dev/null +++ b/openpype/hosts/maya/api/template_loader.py @@ -0,0 +1,242 @@ +from maya import cmds + +from openpype.pipeline import legacy_io +from openpype.lib.abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader +) +from openpype.lib.build_template_exceptions import TemplateAlreadyImported + +PLACEHOLDER_SET = 'PLACEHOLDERS_SET' + + +class MayaTemplateLoader(AbstractTemplateLoader): + """Concrete implementation of AbstractTemplateLoader for maya + """ + + def import_template(self, path): + """Import template into current scene. + Block if a template is already loaded. + Args: + path (str): A path to current template (usually given by + get_template_path implementation) + Returns: + bool: Wether the template was succesfully imported or not + """ + if cmds.objExists(PLACEHOLDER_SET): + raise TemplateAlreadyImported( + "Build template already loaded\n" + "Clean scene if needed (File > New Scene)") + + cmds.sets(name=PLACEHOLDER_SET, empty=True) + self.new_nodes = cmds.file(path, i=True, returnNewNodes=True) + cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True) + + for set in cmds.listSets(allSets=True): + if (cmds.objExists(set) and + cmds.attributeQuery('id', node=set, exists=True) and + cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'): + if cmds.attributeQuery('asset', node=set, exists=True): + cmds.setAttr( + set + '.asset', + legacy_io.Session['AVALON_ASSET'], type='string' + ) + + return True + + def template_already_imported(self, err_msg): + clearButton = "Clear scene and build" + updateButton = "Update template" + abortButton = "Abort" + + title = "Scene already builded" + message = ( + "It's seems a template was already build for this scene.\n" + "Error message reveived :\n\n\"{}\"".format(err_msg)) + buttons = [clearButton, updateButton, abortButton] + defaultButton = clearButton + cancelButton = abortButton + dismissString = abortButton + answer = cmds.confirmDialog( + t=title, + m=message, + b=buttons, + db=defaultButton, + cb=cancelButton, + ds=dismissString) + + if answer == clearButton: + cmds.file(newFile=True, force=True) + self.import_template(self.template_path) + self.populate_template() + elif answer == updateButton: + self.update_missing_containers() + elif answer == abortButton: + return + + @staticmethod + def get_template_nodes(): + attributes = cmds.ls('*.builder_type', long=True) + return [attribute.rpartition('.')[0] for attribute in attributes] + + def get_loaded_containers_by_id(self): + containers = cmds.sets('AVALON_CONTAINERS', q=True) + return [ + cmds.getAttr(container + '.representation') + for container in containers] + + +class MayaPlaceholder(AbstractPlaceholder): + """Concrete implementation of AbstractPlaceholder for maya + """ + + optional_attributes = {'asset', 'subset', 'hierarchy'} + + def get_data(self, node): + user_data = dict() + for attr in self.attributes.union(self.optional_attributes): + attribute_name = '{}.{}'.format(node, attr) + if not cmds.attributeQuery(attr, node=node, exists=True): + print("{} not found".format(attribute_name)) + continue + user_data[attr] = cmds.getAttr( + attribute_name, + asString=True) + user_data['parent'] = ( + cmds.getAttr(node + '.parent', asString=True) + or node.rpartition('|')[0] or "") + user_data['node'] = node + if user_data['parent']: + siblings = cmds.listRelatives(user_data['parent'], children=True) + else: + siblings = cmds.ls(assemblies=True) + node_shortname = user_data['node'].rpartition('|')[2] + current_index = cmds.getAttr(node + '.index', asString=True) + user_data['index'] = ( + current_index if current_index >= 0 + else siblings.index(node_shortname)) + + self.data = user_data + + def parent_in_hierarchy(self, containers): + """Parent loaded container to placeholder's parent + ie : Set loaded content as placeholder's sibling + Args: + containers (String): Placeholder loaded containers + """ + if not containers: + return + + roots = cmds.sets(containers, q=True) + nodes_to_parent = [] + for root in roots: + if root.endswith("_RN"): + refRoot = cmds.referenceQuery(root, n=True)[0] + refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] + nodes_to_parent.extend(refRoot) + elif root in cmds.listSets(allSets=True): + if not cmds.sets(root, q=True): + return + else: + continue + else: + nodes_to_parent.append(root) + + if self.data['parent']: + cmds.parent(nodes_to_parent, self.data['parent']) + # Move loaded nodes to correct index in outliner hierarchy + placeholder_node = self.data['node'] + placeholder_form = cmds.xform( + placeholder_node, + q=True, + matrix=True, + worldSpace=True + ) + for node in set(nodes_to_parent): + cmds.reorder(node, front=True) + cmds.reorder(node, relative=self.data['index']) + cmds.xform(node, matrix=placeholder_form, ws=True) + + holding_sets = cmds.listSets(object=placeholder_node) + if not holding_sets: + return + for holding_set in holding_sets: + cmds.sets(roots, forceElement=holding_set) + + def clean(self): + """Hide placeholder, parent them to root + add them to placeholder set and register placeholder's parent + to keep placeholder info available for future use + """ + node = self.data['node'] + if self.data['parent']: + cmds.setAttr(node + '.parent', self.data['parent'], type='string') + if cmds.getAttr(node + '.index') < 0: + cmds.setAttr(node + '.index', self.data['index']) + + holding_sets = cmds.listSets(object=node) + if holding_sets: + for set in holding_sets: + cmds.sets(node, remove=set) + + if cmds.listRelatives(node, p=True): + node = cmds.parent(node, world=True)[0] + cmds.sets(node, addElement=PLACEHOLDER_SET) + cmds.hide(node) + cmds.setAttr(node + '.hiddenInOutliner', True) + + def convert_to_db_filters(self, current_asset, linked_asset): + if self.data['builder_type'] == "context_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": current_asset, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + elif self.data['builder_type'] == "linked_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": asset_name, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } for asset_name in linked_asset + ] + + else: + return [ + { + "type": "representation", + "context.asset": {"$regex": self.data['asset']}, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + def err_message(self): + return ( + "Error while trying to load a representation.\n" + "Either the subset wasn't published or the template is malformed." + "\n\n" + "Builder was looking for :\n{attributes}".format( + attributes="\n".join([ + "{}: {}".format(key.title(), value) + for key, value in self.data.items()] + ) + ) + ) From 60cc108251db884a04cef1d2ea29a558a7750b8c Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 14:28:28 +0200 Subject: [PATCH 006/155] add build workfile in menu --- openpype/hosts/maya/api/menu.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 8beaf491bb..c66eeb449f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,13 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile +from openpype.api import ( + BuildWorkfile, + # build_workfile_template + # update_workfile_template +) + +from openpype.lib.build_template import build_workfile_template, update_workfile_template from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -158,6 +164,16 @@ def install(): parent=builder_menu, command=lambda *args: update_placeholder() ) + cmds.menuItem( + "Build Workfile from template", + parent=builder_menu, + command=lambda *args: build_workfile_template() + ) + cmds.menuItem( + "Update Workfile from template", + parent=builder_menu, + command=lambda *args: update_workfile_template() + ) cmds.setParent(MENU_NAME, menu=True) From aaa1f13f9d0ae038f70eb2cdc21cba56f92b97dd Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:35:05 +0200 Subject: [PATCH 007/155] delete the task_name verification since it does not exists in the maya menu settings --- openpype/lib/abstract_template_loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 6888cbf757..2dfec1a006 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -157,8 +157,6 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue - if prf['task_names'] and task_name not in prf['task_names']: - continue path = prf['path'] break else: # IF no template were found (no break happened) @@ -253,7 +251,7 @@ class AbstractTemplateLoader: linked_assets) # get representation by assets for db_filter in placeholder_db_filters: - placeholder_representations = list(avalon.io.find(db_filter)) + placeholder_representations = list(legacy_io.find(db_filter)) for representation in reduce(update_representations, placeholder_representations, dict()).values(): From c2aca3422c8c2e29a169f9550e7e1719733f7ec4 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:38:47 +0200 Subject: [PATCH 008/155] rename correctly attributes to correpsond the ones in the placeholders --- openpype/lib/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 2dfec1a006..628d0bd895 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -357,7 +357,7 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'op_family', 'op_representation', + attributes = {'builder_type', 'family', 'representation', 'order', 'loader', 'loader_args'} optional_attributes = {} From 95d3686889470a8ad6d677b949a86cab094e47ea Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Fri, 27 May 2022 12:44:51 +0200 Subject: [PATCH 009/155] create placeholder name dynamically from arguments --- .../hosts/maya/api/lib_template_builder.py | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index d8772f3f9a..ee78f19a3e 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -1,3 +1,4 @@ +import json from collections import OrderedDict import maya.cmds as cmds @@ -30,17 +31,20 @@ def create_placeholder(): if not args: return # operation canceled, no locator created - selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] - if selection: - cmds.parent(placeholder, selection[0]) # custom arg parse to force empty data query # and still imprint them on placeholder # and getting items when arg is of type Enumerator - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() + options = create_options(args) + + # create placeholder name dynamically from args and options + placeholder_name = create_placeholder_name(args, options) + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + + if selection: + cmds.parent(placeholder, selection[0]) + imprint(placeholder, options) # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes @@ -49,13 +53,42 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( placeholder, longName="parent", - hidden=True, dataType="string") + hidden=False, dataType="string") cmds.addAttr( placeholder, longName="index", - hidden=True, attributeType="short", + hidden=False, attributeType="short", defaultValue=-1) +def create_options(args): + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + return options + + +def create_placeholder_name(args, options): + placeholder_builder_type = [ + arg.read() for arg in args if 'builder_type' in str(arg) + ][0] + placeholder_family = options['family'] + placeholder_name = placeholder_builder_type.split('_') + placeholder_name.insert(1, placeholder_family) + + # add loader arguments if any + if options['loader_args']: + pos = 2 + loader_args = options['loader_args'].replace('\'', '\"') + loader_args = json.loads(loader_args) + values = [v for v in loader_args.values()] + for i in range(len(values)): + placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) + + return placeholder_name + + def update_placeholder(): placeholder = cmds.ls(selection=True) if len(placeholder) == 0: From e29d4e5699e6dace616933317c57fcc9bc43c878 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:19:12 +0200 Subject: [PATCH 010/155] minor refactoring --- .../hosts/maya/api/lib_template_builder.py | 19 ++++++++++++++----- openpype/hosts/maya/api/menu.py | 11 +++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index ee78f19a3e..bec0f1fc66 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -52,12 +52,21 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, longName="parent", - hidden=False, dataType="string") + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) cmds.addAttr( - placeholder, longName="index", - hidden=False, attributeType="short", - defaultValue=-1) + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + parents = cmds.ls(selection[0], long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") def create_options(args): diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c66eeb449f..1337713561 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,13 +6,12 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import ( - BuildWorkfile, - # build_workfile_template - # update_workfile_template -) +from openpype.api import BuildWorkfile -from openpype.lib.build_template import build_workfile_template, update_workfile_template +from openpype.lib.build_template import ( + build_workfile_template, + update_workfile_template +) from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools From 28518eeb21f2a9ef56c32c0009ce09aecf871a86 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:20:31 +0200 Subject: [PATCH 011/155] change load method since avalon doesn't exsist anymore --- openpype/lib/abstract_template_loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 628d0bd895..77ba04c4db 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -5,11 +5,10 @@ import traceback import six -import openpype from openpype.settings import get_project_settings from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, load from functools import reduce @@ -271,9 +270,10 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - return openpype.pipeline.load( + repre = load.get_representation_context(last_representation) + return load.load_with_repre_context( loaders_by_name[placeholder.loader], - last_representation['_id'], + repre, options=parse_loader_args(placeholder.data['loader_args'])) def load_succeed(self, placeholder, container): From b65a1d4e79e3fa2ff4ca11392f9ccbce68a19a78 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:53:49 +0200 Subject: [PATCH 012/155] fix update placeholder --- .../hosts/maya/api/lib_template_builder.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bec0f1fc66..2efc210d10 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,14 +69,6 @@ def create_placeholder(): cmds.setAttr(placeholder + '.parent', parents[0], type="string") -def create_options(args): - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() - return options - - def create_placeholder_name(args, options): placeholder_builder_type = [ arg.read() for arg in args if 'builder_type' in str(arg) @@ -112,12 +104,38 @@ def update_placeholder(): if not args: return # operation canceled + options = create_options(args) + + imprint(placeholder, options) + imprint_enum(placeholder, args) + + cmds.addAttr( + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) + cmds.addAttr( + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + selected = cmds.ls(selection=True, long=True) + selected = selected[0].split('|')[-2] + selected = cmds.ls(selected) + parents = cmds.ls(selected, long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") + + +def create_options(args): options = OrderedDict() for arg in args: if not type(arg) == qargparse.Separator: options[str(arg)] = arg._data.get("items") or arg.read() - imprint(placeholder, options) - imprint_enum(placeholder, args) + return options def imprint_enum(placeholder, args): From b095249fb859c9845d00efb8d69bd515867c6e94 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 10:40:44 +0200 Subject: [PATCH 013/155] change menu command for build and update workfile from template --- openpype/hosts/maya/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 1337713561..c0bad7092f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -166,12 +166,12 @@ def install(): cmds.menuItem( "Build Workfile from template", parent=builder_menu, - command=lambda *args: build_workfile_template() + command=build_workfile_template ) cmds.menuItem( "Update Workfile from template", parent=builder_menu, - command=lambda *args: update_workfile_template() + command=update_workfile_template ) cmds.setParent(MENU_NAME, menu=True) From 79c9dc94528ff8f3ae216f106b2225ae790fb044 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 12:22:06 +0200 Subject: [PATCH 014/155] get full name placeholder to avoid any conflict between two placeholders with same short name --- .../hosts/maya/api/lib_template_builder.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 2efc210d10..108988a676 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,33 +40,37 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + placeholder = cmds.spaceLocator(name=placeholder_name)[0] + + # get the long name of the placeholder (with the groups) + placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) - imprint(placeholder, options) + imprint(placeholder_full_name, options) + # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes - imprint_enum(placeholder, args) + imprint_enum(placeholder_full_name, args) # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, + placeholder_full_name, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( - placeholder, + placeholder_full_name, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") def create_placeholder_name(args, options): @@ -75,7 +79,10 @@ def create_placeholder_name(args, options): ][0] placeholder_family = options['family'] placeholder_name = placeholder_builder_type.split('_') - placeholder_name.insert(1, placeholder_family) + + # add famlily in any + if placeholder_family: + placeholder_name.insert(1, placeholder_family) # add loader arguments if any if options['loader_args']: @@ -85,9 +92,10 @@ def create_placeholder_name(args, options): values = [v for v in loader_args.values()] for i in range(len(values)): placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) - return placeholder_name + return placeholder_name.capitalize() def update_placeholder(): @@ -112,13 +120,13 @@ def update_placeholder(): cmds.addAttr( placeholder, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( placeholder, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) From aa7e7093df8d72357118bdb34dbe03e4e73d6801 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:46:24 +0200 Subject: [PATCH 015/155] add a log if no reprensation found for the current placeholder --- openpype/lib/abstract_template_loader.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 77ba04c4db..cd0416426c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -19,6 +19,10 @@ from openpype.lib.build_template_exceptions import ( TemplateNotFound ) +import logging + +log = logging.getLogger(__name__) + def update_representations(entities, entity): if entity['context']['subset'] not in entities: @@ -215,8 +219,15 @@ class AbstractTemplateLoader: current_asset, linked_assets ) - for representation in placeholder_representations: + if not placeholder_representations: + self.log.info( + "There's no representation for this placeholder: " + "{}".format(placeholder.data['node']) + ) + continue + + for representation in placeholder_representations: self.preload(placeholder, loaders_by_name, representation) if self.load_data_is_incorrect( From f50999d0927bf533a74417479e4cdb4a06b32b3d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:53:59 +0200 Subject: [PATCH 016/155] add debug logs for placeholders --- openpype/lib/abstract_template_loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index cd0416426c..159d5c8f6c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -213,7 +213,13 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() + self.log.debug("Placeholders found in template: {}".format( + [placeholder.data['node'] for placeholder in placeholders] + )) for placeholder in placeholders: + self.log.debug("Start to processing placeholder {}".format( + placeholder.data['node'] + )) placeholder_representations = self.get_placeholder_representations( placeholder, current_asset, From edb55949df619a81c1828571030634a4b0c49584 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 12:56:24 +0200 Subject: [PATCH 017/155] refactor avalon imports from lib_template_builder --- .../hosts/maya/api/lib_template_builder.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 openpype/hosts/maya/api/lib_template_builder.py diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py new file mode 100644 index 0000000000..172a6f9b2b --- /dev/null +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -0,0 +1,184 @@ +from collections import OrderedDict +import maya.cmds as cmds + +import qargparse +from openpype.tools.utils.widgets import OptionDialog +from lib import get_main_window, imprint + +# To change as enum +build_types = ["context_asset", "linked_asset", "all_assets"] + + +def get_placeholder_attributes(node): + return { + attr: cmds.getAttr("{}.{}".format(node, attr)) + for attr in cmds.listAttr(node, userDefined=True)} + + +def delete_placeholder_attributes(node): + ''' + function to delete all extra placeholder attributes + ''' + extra_attributes = get_placeholder_attributes(node) + for attribute in extra_attributes: + cmds.deleteAttr(node + '.' + attribute) + + +def create_placeholder(): + args = placeholder_window() + + if not args: + return # operation canceled, no locator created + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] + if selection: + cmds.parent(placeholder, selection[0]) + # custom arg parse to force empty data query + # and still imprint them on placeholder + # and getting items when arg is of type Enumerator + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + # Some tweaks because imprint force enums to to default value so we get + # back arg read and force them to attributes + imprint_enum(placeholder, args) + + # Add helper attributes to keep placeholder info + cmds.addAttr( + placeholder, longName="parent", + hidden=True, dataType="string") + cmds.addAttr( + placeholder, longName="index", + hidden=True, attributeType="short", + defaultValue=-1) + + +def update_placeholder(): + placeholder = cmds.ls(selection=True) + if len(placeholder) == 0: + raise ValueError("No node selected") + if len(placeholder) > 1: + raise ValueError("Too many selected nodes") + placeholder = placeholder[0] + + args = placeholder_window(get_placeholder_attributes(placeholder)) + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + if not args: + return # operation canceled + + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + imprint_enum(placeholder, args) + + +def imprint_enum(placeholder, args): + """ + Imprint method doesn't act properly with enums. + Replacing the functionnality with this for now + """ + enum_values = {str(arg): arg.read() + for arg in args if arg._data.get("items")} + string_to_value_enum_table = { + build: i for i, build + in enumerate(build_types)} + for key, value in enum_values.items(): + cmds.setAttr( + placeholder + "." + key, + string_to_value_enum_table[value]) + + +def placeholder_window(options=None): + options = options or dict() + dialog = OptionDialog(parent=get_main_window()) + dialog.setWindowTitle("Create Placeholder") + + args = [ + qargparse.Separator("Main attributes"), + qargparse.Enum( + "builder_type", + label="Asset Builder Type", + default=options.get("builder_type", 0), + items=build_types, + help="""Asset Builder Type +Builder type describe what template loader will look for. +context_asset : Template loader will look for subsets of +current context asset (Asset bob will find asset) +linked_asset : Template loader will look for assets linked +to current context asset. +Linked asset are looked in avalon database under field "inputLinks" +""" + ), + qargparse.String( + "family", + default=options.get("family", ""), + label="OpenPype Family", + placeholder="ex: model, look ..."), + qargparse.String( + "representation", + default=options.get("representation", ""), + label="OpenPype Representation", + placeholder="ex: ma, abc ..."), + qargparse.String( + "loader", + default=options.get("loader", ""), + label="Loader", + placeholder="ex: ReferenceLoader, LightLoader ...", + help="""Loader +Defines what openpype loader will be used to load assets. +Useable loader depends on current host's loader list. +Field is case sensitive. +"""), + qargparse.String( + "loader_args", + default=options.get("loader_args", ""), + label="Loader Arguments", + placeholder='ex: {"camera":"persp", "lights":True}', + help="""Loader +Defines a dictionnary of arguments used to load assets. +Useable arguments depend on current placeholder Loader. +Field should be a valid python dict. Anything else will be ignored. +"""), + qargparse.Integer( + "order", + default=options.get("order", 0), + min=0, + max=999, + label="Order", + placeholder="ex: 0, 100 ... (smallest order loaded first)", + help="""Order +Order defines asset loading priority (0 to 999) +Priority rule is : "lowest is first to load"."""), + qargparse.Separator( + "Optional attributes"), + qargparse.String( + "asset", + default=options.get("asset", ""), + label="Asset filter", + placeholder="regex filtering by asset name", + help="Filtering assets by matching field regex to asset's name"), + qargparse.String( + "subset", + default=options.get("subset", ""), + label="Subset filter", + placeholder="regex filtering by subset name", + help="Filtering assets by matching field regex to subset's name"), + qargparse.String( + "hierarchy", + default=options.get("hierarchy", ""), + label="Hierarchy filter", + placeholder="regex filtering by asset's hierarchy", + help="Filtering assets by matching field asset's hierarchy") + ] + dialog.create(args) + + if not dialog.exec_(): + return None + + return args From 15e51cd6a640aea61eb927b84ce6b48990d206f3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 14:58:27 +0200 Subject: [PATCH 018/155] add the templated wrokfile build schema for maya --- .../defaults/project_settings/maya.json | 8 +++++ .../projects_schema/schema_project_maya.json | 4 +++ .../schema_templated_workfile_build.json | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index efd22e13c8..2e0e30b74b 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -718,6 +718,14 @@ } ] }, + "templated_workfile_build": { + "profiles": [ + { + "task_types": [], + "path": "/path/to/your/template" + } + ] + }, "filters": { "preset 1": { "ValidateNoAnimation": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 40e98b0333..d137049e9e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -73,6 +73,10 @@ "type": "schema", "name": "schema_workfile_build" }, + { + "type": "schema", + "name": "schema_templated_workfile_build" + }, { "type": "schema", "name": "schema_publish_gui_filter" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json new file mode 100644 index 0000000000..01e74f64b0 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "path", + "label": "Path to template", + "type": "text", + "object_type": "text" + } + ] + } + } + ] +} From c8c36144cb26df5d0024fcd02df265736bbd209f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 17:28:42 +0200 Subject: [PATCH 019/155] add placeholder menu to maya --- .../hosts/maya/api/lib_template_builder.py | 2 +- openpype/hosts/maya/api/menu.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 172a6f9b2b..d8772f3f9a 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -3,7 +3,7 @@ import maya.cmds as cmds import qargparse from openpype.tools.utils.widgets import OptionDialog -from lib import get_main_window, imprint +from .lib import get_main_window, imprint # To change as enum build_types = ["context_asset", "linked_asset", "all_assets"] diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 97f06c43af..8beaf491bb 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -11,8 +11,10 @@ from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib + from .lib import get_main_window, IS_HEADLESS from .commands import reset_frame_range +from .lib_template_builder import create_placeholder, update_placeholder log = logging.getLogger(__name__) @@ -139,6 +141,24 @@ def install(): parent_widget ) ) + + builder_menu = cmds.menuItem( + "Template Builder", + subMenu=True, + tearOff=True, + parent=MENU_NAME + ) + cmds.menuItem( + "Create Placeholder", + parent=builder_menu, + command=lambda *args: create_placeholder() + ) + cmds.menuItem( + "Update Placeholder", + parent=builder_menu, + command=lambda *args: update_placeholder() + ) + cmds.setParent(MENU_NAME, menu=True) def add_scripts_menu(): From 770b6d3ab2ee9e3bdf460cee4fdba96d67e44fb2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:25:39 +0200 Subject: [PATCH 020/155] setup build template in openpype lib --- openpype/lib/__init__.py | 2 + openpype/lib/abstract_template_loader.py | 447 ++++++++++++++++++++++ openpype/lib/avalon_context.py | 222 +++++------ openpype/lib/build_template.py | 61 +++ openpype/lib/build_template_exceptions.py | 35 ++ 5 files changed, 660 insertions(+), 107 deletions(-) create mode 100644 openpype/lib/abstract_template_loader.py create mode 100644 openpype/lib/build_template.py create mode 100644 openpype/lib/build_template_exceptions.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 8d4e733b7d..8f3919d378 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -136,6 +136,7 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, + get_loaders_by_name, BuildWorkfile, @@ -308,6 +309,7 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", + "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py new file mode 100644 index 0000000000..6888cbf757 --- /dev/null +++ b/openpype/lib/abstract_template_loader.py @@ -0,0 +1,447 @@ +import os +from abc import ABCMeta, abstractmethod + +import traceback + +import six + +import openpype +from openpype.settings import get_project_settings +from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name +from openpype.api import PypeLogger as Logger +from openpype.pipeline import legacy_io + +from functools import reduce + +from openpype.lib.build_template_exceptions import ( + TemplateAlreadyImported, + TemplateLoadingFailed, + TemplateProfileNotFound, + TemplateNotFound +) + + +def update_representations(entities, entity): + if entity['context']['subset'] not in entities: + entities[entity['context']['subset']] = entity + else: + current = entities[entity['context']['subset']] + incomming = entity + entities[entity['context']['subset']] = max( + current, incomming, + key=lambda entity: entity["context"].get("version", -1)) + + return entities + + +def parse_loader_args(loader_args): + if not loader_args: + return dict() + try: + parsed_args = eval(loader_args) + if not isinstance(parsed_args, dict): + return dict() + else: + return parsed_args + except Exception as err: + print( + "Error while parsing loader arguments '{}'.\n{}: {}\n\n" + "Continuing with default arguments. . .".format( + loader_args, + err.__class__.__name__, + err)) + return dict() + + +@six.add_metaclass(ABCMeta) +class AbstractTemplateLoader: + """ + Abstraction of Template Loader. + Properties: + template_path : property to get current template path + Methods: + import_template : Abstract Method. Used to load template, + depending on current host + get_template_nodes : Abstract Method. Used to query nodes acting + as placeholders. Depending on current host + """ + + def __init__(self, placeholder_class): + + self.loaders_by_name = get_loaders_by_name() + self.current_asset = legacy_io.Session["AVALON_ASSET"] + self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.host_name = legacy_io.Session["AVALON_APP"] + self.task_name = legacy_io.Session["AVALON_TASK"] + self.placeholder_class = placeholder_class + self.current_asset_docs = legacy_io.find_one({ + "type": "asset", + "name": self.current_asset + }) + self.task_type = ( + self.current_asset_docs + .get("data", {}) + .get("tasks", {}) + .get(self.task_name, {}) + .get("type") + ) + + self.log = Logger().get_logger("BUILD TEMPLATE") + + self.log.info( + "BUILDING ASSET FROM TEMPLATE :\n" + "Starting templated build for {asset} in {project}\n\n" + "Asset : {asset}\n" + "Task : {task_name} ({task_type})\n" + "Host : {host}\n" + "Project : {project}\n".format( + asset=self.current_asset, + host=self.host_name, + project=self.project_name, + task_name=self.task_name, + task_type=self.task_type + )) + # Skip if there is no loader + if not self.loaders_by_name: + self.log.warning( + "There is no registered loaders. No assets will be loaded") + return + + def template_already_imported(self, err_msg): + """In case template was already loaded. + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case.""" + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateAlreadyImported(err_msg) + + def template_loading_failed(self, err_msg): + """In case template loading failed + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case. + """ + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateLoadingFailed(err_msg) + + @property + def template_path(self): + """ + Property returning template path. Avoiding setter. + Getting template path from open pype settings based on current avalon + session and solving the path variables if needed. + Returns: + str: Solved template path + Raises: + TemplateProfileNotFound: No profile found from settings for + current avalon session + KeyError: Could not solve path because a key does not exists + in avalon context + TemplateNotFound: Solved path does not exists on current filesystem + """ + project_name = self.project_name + host_name = self.host_name + task_name = self.task_name + task_type = self.task_type + + anatomy = Anatomy(project_name) + project_settings = get_project_settings(project_name) + + build_info = project_settings[host_name]['templated_workfile_build'] + profiles = build_info['profiles'] + + for prf in profiles: + if prf['task_types'] and task_type not in prf['task_types']: + continue + if prf['task_names'] and task_name not in prf['task_names']: + continue + path = prf['path'] + break + else: # IF no template were found (no break happened) + raise TemplateProfileNotFound( + "No matching profile found for task '{}' of type '{}' " + "with host '{}'".format(task_name, task_type, host_name) + ) + if path is None: + raise TemplateLoadingFailed( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles".format(host_name.title())) + try: + solved_path = None + while True: + solved_path = anatomy.path_remapper(path) + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + finally: + solved_path = os.path.normpath(solved_path) + + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in openPype settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + self.log.info("Found template at : '{}'".format(solved_path)) + + return solved_path + + def populate_template(self, ignored_ids=None): + """ + Use template placeholders to load assets and parent them in hierarchy + Arguments : + ignored_ids : + Returns: + None + """ + loaders_by_name = self.loaders_by_name + current_asset = self.current_asset + linked_assets = [asset['name'] for asset + in get_linked_assets(self.current_asset_docs)] + + ignored_ids = ignored_ids or [] + placeholders = self.get_placeholders() + for placeholder in placeholders: + placeholder_representations = self.get_placeholder_representations( + placeholder, + current_asset, + linked_assets + ) + for representation in placeholder_representations: + + self.preload(placeholder, loaders_by_name, representation) + + if self.load_data_is_incorrect( + placeholder, + representation, + ignored_ids): + continue + + self.log.info( + "Loading {}_{} with loader {}\n" + "Loader arguments used : {}".format( + representation['context']['asset'], + representation['context']['subset'], + placeholder.loader, + placeholder.data['loader_args'])) + + try: + container = self.load( + placeholder, loaders_by_name, representation) + except Exception: + self.load_failed(placeholder, representation) + else: + self.load_succeed(placeholder, container) + finally: + self.postload(placeholder) + + def get_placeholder_representations( + self, placeholder, current_asset, linked_assets): + placeholder_db_filters = placeholder.convert_to_db_filters( + current_asset, + linked_assets) + # get representation by assets + for db_filter in placeholder_db_filters: + placeholder_representations = list(avalon.io.find(db_filter)) + for representation in reduce(update_representations, + placeholder_representations, + dict()).values(): + yield representation + + def load_data_is_incorrect( + self, placeholder, last_representation, ignored_ids): + if not last_representation: + self.log.warning(placeholder.err_message()) + return True + if (str(last_representation['_id']) in ignored_ids): + print("Ignoring : ", last_representation['_id']) + return True + return False + + def preload(self, placeholder, loaders_by_name, last_representation): + pass + + def load(self, placeholder, loaders_by_name, last_representation): + return openpype.pipeline.load( + loaders_by_name[placeholder.loader], + last_representation['_id'], + options=parse_loader_args(placeholder.data['loader_args'])) + + def load_succeed(self, placeholder, container): + placeholder.parent_in_hierarchy(container) + + def load_failed(self, placeholder, last_representation): + self.log.warning("Got error trying to load {}:{} with {}\n\n" + "{}".format(last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader, + traceback.format_exc())) + + def postload(self, placeholder): + placeholder.clean() + + def update_missing_containers(self): + loaded_containers_ids = self.get_loaded_containers_by_id() + self.populate_template(ignored_ids=loaded_containers_ids) + + def get_placeholders(self): + placeholder_class = self.placeholder_class + placeholders = map(placeholder_class, self.get_template_nodes()) + valid_placeholders = filter(placeholder_class.is_valid, placeholders) + sorted_placeholders = sorted(valid_placeholders, + key=placeholder_class.order) + return sorted_placeholders + + @abstractmethod + def get_loaded_containers_by_id(self): + """ + Collect already loaded containers for updating scene + Return: + dict (string, node): A dictionnary id as key + and containers as value + """ + pass + + @abstractmethod + def import_template(self, template_path): + """ + Import template in current host + Args: + template_path (str): fullpath to current task and + host's template file + Return: + None + """ + pass + + @abstractmethod + def get_template_nodes(self): + """ + Returning a list of nodes acting as host placeholders for + templating. The data representation is by user. + AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes + Args : + None + Returns: + list(AnyNode): Solved template path + """ + pass + + +@six.add_metaclass(ABCMeta) +class AbstractPlaceholder: + """Abstraction of placeholders logic + Properties: + attributes: A list of mandatory attribute to decribe placeholder + and assets to load. + optional_attributes: A list of optional attribute to decribe + placeholder and assets to load + loader: Name of linked loader to use while loading assets + is_context: Is placeholder linked + to context asset (or to linked assets) + Methods: + is_repres_valid: + loader: + order: + is_valid: + get_data: + parent_in_hierachy: + """ + + attributes = {'builder_type', 'op_family', 'op_representation', + 'order', 'loader', 'loader_args'} + optional_attributes = {} + + def __init__(self, node): + self.get_data(node) + + def order(self): + """Get placeholder order. + Order is used to sort them by priority + Priority is lowset first, highest last + (ex: + 1: First to load + 100: Last to load) + Returns: + Int: Order priority + """ + return self.data.get('order') + + @property + def loader(self): + """Return placeholder loader type + Returns: + string: Loader name + """ + return self.data.get('loader') + + @property + def is_context(self): + """Return placeholder type + context_asset: For loading current asset + linked_asset: For loading linked assets + Returns: + bool: true if placeholder is a context placeholder + """ + return self.data.get('builder_type') == 'context_asset' + + def is_valid(self): + """Test validity of placeholder + i.e.: every attributes exists in placeholder data + Returns: + Bool: True if every attributes are a key of data + """ + if set(self.attributes).issubset(self.data.keys()): + print("Valid placeholder : {}".format(self.data["node"])) + return True + print("Placeholder is not valid : {}".format(self.data["node"])) + return False + + @abstractmethod + def parent_in_hierarchy(self, containers): + """Place container in correct hierarchy + given by placeholder + Args: + containers (String): Container name returned back by + placeholder's loader. + """ + pass + + @abstractmethod + def clean(self): + """Clean placeholder from hierarchy after loading assets. + """ + pass + + @abstractmethod + def convert_to_db_filters(self, current_asset, linked_asset): + """map current placeholder data as a db filter + args: + current_asset (String): Name of current asset in context + linked asset (list[String]) : Names of assets linked to + current asset in context + Returns: + dict: a dictionnary describing a filter to look for asset in + a database + """ + pass + + @abstractmethod + def get_data(self, node): + """ + Collect placeholders information. + Args: + node (AnyNode): A unique node decided by Placeholder implementation + """ + pass diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 9d8a92cfe9..8c80b4a4ae 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,6 +15,7 @@ from openpype.settings import ( get_project_settings, get_system_settings ) + from .anatomy import Anatomy from .profiles_filtering import filter_profiles from .events import emit_event @@ -922,6 +923,118 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): ) +@with_pipeline_io +def collect_last_version_repres(asset_entities): + """Collect subsets, versions and representations for asset_entities. + + Args: + asset_entities (list): Asset entities for which want to find data + + Returns: + (dict): collected entities + + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... + } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(legacy_io.find({ + "type": "subset", + "parent": {"$in": list(asset_entity_by_ids.keys())} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(legacy_io.find({ + "type": "version", + "parent": {"$in": list(subset_entity_by_ids.keys())} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = legacy_io.find({ + "type": "representation", + "parent": {"$in": list(last_versions_by_id.keys())} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output + + +@with_pipeline_io +def get_loaders_by_name(): + from openpype.pipeline import discover_loader_plugins + + loaders_by_name = {} + for loader in discover_loader_plugins(): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + class BuildWorkfile: """Wrapper for build workfile process. @@ -979,8 +1092,6 @@ class BuildWorkfile: ... }] """ - from openpype.pipeline import discover_loader_plugins - # Get current asset name and entity current_asset_name = legacy_io.Session["AVALON_ASSET"] current_asset_entity = legacy_io.find_one({ @@ -996,14 +1107,7 @@ class BuildWorkfile: return # Prepare available loaders - loaders_by_name = {} - for loader in discover_loader_plugins(): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError( - "Duplicated loader name {0}!".format(loader_name) - ) - loaders_by_name[loader_name] = loader + loaders_by_name = get_loaders_by_name() # Skip if there are any loaders if not loaders_by_name: @@ -1075,7 +1179,7 @@ class BuildWorkfile: return # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(assets) + prepared_entities = collect_last_version_repres(assets) # Load containers by prepared entities and presets loaded_containers = [] @@ -1491,102 +1595,6 @@ class BuildWorkfile: return loaded_containers - @with_pipeline_io - def _collect_last_version_repres(self, asset_entities): - """Collect subsets, versions and representations for asset_entities. - - Args: - asset_entities (list): Asset entities for which want to find data - - Returns: - (dict): collected entities - - Example output: - ``` - { - {Asset ID}: { - "asset_entity": , - "subsets": { - {Subset ID}: { - "subset_entity": , - "version": { - "version_entity": , - "repres": [ - , , ... - ] - } - }, - ... - } - }, - ... - } - output[asset_id]["subsets"][subset_id]["version"]["repres"] - ``` - """ - - if not asset_entities: - return {} - - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) - - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version - - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) - - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, - "version": { - "version_entity": version, - "repres": [] - } - } - - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre - ) - - return output - @with_pipeline_io def get_creator_by_name(creator_name, case_sensitive=False): diff --git a/openpype/lib/build_template.py b/openpype/lib/build_template.py new file mode 100644 index 0000000000..7f749cbec2 --- /dev/null +++ b/openpype/lib/build_template.py @@ -0,0 +1,61 @@ +from openpype.pipeline import registered_host +from openpype.lib import classes_from_module +from importlib import import_module + +from .abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader) + +from .build_template_exceptions import ( + TemplateLoadingFailed, + TemplateAlreadyImported, + MissingHostTemplateModule, + MissingTemplatePlaceholderClass, + MissingTemplateLoaderClass +) + +_module_path_format = 'openpype.{host}.template_loader' + + +def build_workfile_template(*args): + template_loader = build_template_loader() + try: + template_loader.import_template(template_loader.template_path) + except TemplateAlreadyImported as err: + template_loader.template_already_imported(err) + except TemplateLoadingFailed as err: + template_loader.template_loading_failed(err) + else: + template_loader.populate_template() + + +def update_workfile_template(args): + template_loader = build_template_loader() + template_loader.update_missing_containers() + + +def build_template_loader(): + host_name = registered_host().__name__.partition('.')[2] + module_path = _module_path_format.format(host=host_name) + module = import_module(module_path) + if not module: + raise MissingHostTemplateModule( + "No template loader found for host {}".format(host_name)) + + template_loader_class = classes_from_module( + AbstractTemplateLoader, + module + ) + template_placeholder_class = classes_from_module( + AbstractPlaceholder, + module + ) + + if not template_loader_class: + raise MissingTemplateLoaderClass() + template_loader_class = template_loader_class[0] + + if not template_placeholder_class: + raise MissingTemplatePlaceholderClass() + template_placeholder_class = template_placeholder_class[0] + return template_loader_class(template_placeholder_class) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py new file mode 100644 index 0000000000..d781eff204 --- /dev/null +++ b/openpype/lib/build_template_exceptions.py @@ -0,0 +1,35 @@ +class MissingHostTemplateModule(Exception): + """Error raised when expected module does not exists""" + pass + + +class MissingTemplatePlaceholderClass(Exception): + """Error raised when module doesn't implement a placeholder class""" + pass + + +class MissingTemplateLoaderClass(Exception): + """Error raised when module doesn't implement a template loader class""" + pass + + +class TemplateNotFound(Exception): + """Exception raised when template does not exist.""" + pass + + +class TemplateProfileNotFound(Exception): + """Exception raised when current profile + doesn't match any template profile""" + pass + + +class TemplateAlreadyImported(Exception): + """Error raised when Template was already imported by host for + this session""" + pass + + +class TemplateLoadingFailed(Exception): + """Error raised whend Template loader was unable to load the template""" + pass \ No newline at end of file From a5a3685f2b5b99bbd1f8de78581eb17af0175ed3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:52:44 +0200 Subject: [PATCH 021/155] add template loader module --- openpype/hosts/maya/api/template_loader.py | 242 +++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 openpype/hosts/maya/api/template_loader.py diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py new file mode 100644 index 0000000000..0e346ca411 --- /dev/null +++ b/openpype/hosts/maya/api/template_loader.py @@ -0,0 +1,242 @@ +from maya import cmds + +from openpype.pipeline import legacy_io +from openpype.lib.abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader +) +from openpype.lib.build_template_exceptions import TemplateAlreadyImported + +PLACEHOLDER_SET = 'PLACEHOLDERS_SET' + + +class MayaTemplateLoader(AbstractTemplateLoader): + """Concrete implementation of AbstractTemplateLoader for maya + """ + + def import_template(self, path): + """Import template into current scene. + Block if a template is already loaded. + Args: + path (str): A path to current template (usually given by + get_template_path implementation) + Returns: + bool: Wether the template was succesfully imported or not + """ + if cmds.objExists(PLACEHOLDER_SET): + raise TemplateAlreadyImported( + "Build template already loaded\n" + "Clean scene if needed (File > New Scene)") + + cmds.sets(name=PLACEHOLDER_SET, empty=True) + self.new_nodes = cmds.file(path, i=True, returnNewNodes=True) + cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True) + + for set in cmds.listSets(allSets=True): + if (cmds.objExists(set) and + cmds.attributeQuery('id', node=set, exists=True) and + cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'): + if cmds.attributeQuery('asset', node=set, exists=True): + cmds.setAttr( + set + '.asset', + legacy_io.Session['AVALON_ASSET'], type='string' + ) + + return True + + def template_already_imported(self, err_msg): + clearButton = "Clear scene and build" + updateButton = "Update template" + abortButton = "Abort" + + title = "Scene already builded" + message = ( + "It's seems a template was already build for this scene.\n" + "Error message reveived :\n\n\"{}\"".format(err_msg)) + buttons = [clearButton, updateButton, abortButton] + defaultButton = clearButton + cancelButton = abortButton + dismissString = abortButton + answer = cmds.confirmDialog( + t=title, + m=message, + b=buttons, + db=defaultButton, + cb=cancelButton, + ds=dismissString) + + if answer == clearButton: + cmds.file(newFile=True, force=True) + self.import_template(self.template_path) + self.populate_template() + elif answer == updateButton: + self.update_missing_containers() + elif answer == abortButton: + return + + @staticmethod + def get_template_nodes(): + attributes = cmds.ls('*.builder_type', long=True) + return [attribute.rpartition('.')[0] for attribute in attributes] + + def get_loaded_containers_by_id(self): + containers = cmds.sets('AVALON_CONTAINERS', q=True) + return [ + cmds.getAttr(container + '.representation') + for container in containers] + + +class MayaPlaceholder(AbstractPlaceholder): + """Concrete implementation of AbstractPlaceholder for maya + """ + + optional_attributes = {'asset', 'subset', 'hierarchy'} + + def get_data(self, node): + user_data = dict() + for attr in self.attributes.union(self.optional_attributes): + attribute_name = '{}.{}'.format(node, attr) + if not cmds.attributeQuery(attr, node=node, exists=True): + print("{} not found".format(attribute_name)) + continue + user_data[attr] = cmds.getAttr( + attribute_name, + asString=True) + user_data['parent'] = ( + cmds.getAttr(node + '.parent', asString=True) + or node.rpartition('|')[0] or "") + user_data['node'] = node + if user_data['parent']: + siblings = cmds.listRelatives(user_data['parent'], children=True) + else: + siblings = cmds.ls(assemblies=True) + node_shortname = user_data['node'].rpartition('|')[2] + current_index = cmds.getAttr(node + '.index', asString=True) + user_data['index'] = ( + current_index if current_index >= 0 + else siblings.index(node_shortname)) + + self.data = user_data + + def parent_in_hierarchy(self, containers): + """Parent loaded container to placeholder's parent + ie : Set loaded content as placeholder's sibling + Args: + containers (String): Placeholder loaded containers + """ + if not containers: + return + + roots = cmds.sets(containers, q=True) + nodes_to_parent = [] + for root in roots: + if root.endswith("_RN"): + refRoot = cmds.referenceQuery(root, n=True)[0] + refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] + nodes_to_parent.extend(refRoot) + elif root in cmds.listSets(allSets=True): + if not cmds.sets(root, q=True): + return + else: + continue + else: + nodes_to_parent.append(root) + + if self.data['parent']: + cmds.parent(nodes_to_parent, self.data['parent']) + # Move loaded nodes to correct index in outliner hierarchy + placeholder_node = self.data['node'] + placeholder_form = cmds.xform( + placeholder_node, + q=True, + matrix=True, + worldSpace=True + ) + for node in set(nodes_to_parent): + cmds.reorder(node, front=True) + cmds.reorder(node, relative=self.data['index']) + cmds.xform(node, matrix=placeholder_form, ws=True) + + holding_sets = cmds.listSets(object=placeholder_node) + if not holding_sets: + return + for holding_set in holding_sets: + cmds.sets(roots, forceElement=holding_set) + + def clean(self): + """Hide placeholder, parent them to root + add them to placeholder set and register placeholder's parent + to keep placeholder info available for future use + """ + node = self.data['node'] + if self.data['parent']: + cmds.setAttr(node + '.parent', self.data['parent'], type='string') + if cmds.getAttr(node + '.index') < 0: + cmds.setAttr(node + '.index', self.data['index']) + + holding_sets = cmds.listSets(object=node) + if holding_sets: + for set in holding_sets: + cmds.sets(node, remove=set) + + if cmds.listRelatives(node, p=True): + node = cmds.parent(node, world=True)[0] + cmds.sets(node, addElement=PLACEHOLDER_SET) + cmds.hide(node) + cmds.setAttr(node + '.hiddenInOutliner', True) + + def convert_to_db_filters(self, current_asset, linked_asset): + if self.data['builder_type'] == "context_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": current_asset, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + elif self.data['builder_type'] == "linked_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": asset_name, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } for asset_name in linked_asset + ] + + else: + return [ + { + "type": "representation", + "context.asset": {"$regex": self.data['asset']}, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + def err_message(self): + return ( + "Error while trying to load a representation.\n" + "Either the subset wasn't published or the template is malformed." + "\n\n" + "Builder was looking for :\n{attributes}".format( + attributes="\n".join([ + "{}: {}".format(key.title(), value) + for key, value in self.data.items()] + ) + ) + ) From f2ae0ffa5950d922fd3cb90ce8bbf30ec64ca0b7 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 14:28:28 +0200 Subject: [PATCH 022/155] add build workfile in menu --- openpype/hosts/maya/api/menu.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 8beaf491bb..c66eeb449f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,13 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile +from openpype.api import ( + BuildWorkfile, + # build_workfile_template + # update_workfile_template +) + +from openpype.lib.build_template import build_workfile_template, update_workfile_template from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -158,6 +164,16 @@ def install(): parent=builder_menu, command=lambda *args: update_placeholder() ) + cmds.menuItem( + "Build Workfile from template", + parent=builder_menu, + command=lambda *args: build_workfile_template() + ) + cmds.menuItem( + "Update Workfile from template", + parent=builder_menu, + command=lambda *args: update_workfile_template() + ) cmds.setParent(MENU_NAME, menu=True) From 41a47bb2bfb9b728f0cad37f5614e5d382b2d9d1 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:35:05 +0200 Subject: [PATCH 023/155] delete the task_name verification since it does not exists in the maya menu settings --- openpype/lib/abstract_template_loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 6888cbf757..2dfec1a006 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -157,8 +157,6 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue - if prf['task_names'] and task_name not in prf['task_names']: - continue path = prf['path'] break else: # IF no template were found (no break happened) @@ -253,7 +251,7 @@ class AbstractTemplateLoader: linked_assets) # get representation by assets for db_filter in placeholder_db_filters: - placeholder_representations = list(avalon.io.find(db_filter)) + placeholder_representations = list(legacy_io.find(db_filter)) for representation in reduce(update_representations, placeholder_representations, dict()).values(): From 58814d21e4688fbb13d183fab7ba9010c68b57f8 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:38:47 +0200 Subject: [PATCH 024/155] rename correctly attributes to correpsond the ones in the placeholders --- openpype/lib/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 2dfec1a006..628d0bd895 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -357,7 +357,7 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'op_family', 'op_representation', + attributes = {'builder_type', 'family', 'representation', 'order', 'loader', 'loader_args'} optional_attributes = {} From 6cb037d3d63290752bacc0aa8c2b81cac8e3b370 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Fri, 27 May 2022 12:44:51 +0200 Subject: [PATCH 025/155] create placeholder name dynamically from arguments --- .../hosts/maya/api/lib_template_builder.py | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index d8772f3f9a..ee78f19a3e 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -1,3 +1,4 @@ +import json from collections import OrderedDict import maya.cmds as cmds @@ -30,17 +31,20 @@ def create_placeholder(): if not args: return # operation canceled, no locator created - selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] - if selection: - cmds.parent(placeholder, selection[0]) # custom arg parse to force empty data query # and still imprint them on placeholder # and getting items when arg is of type Enumerator - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() + options = create_options(args) + + # create placeholder name dynamically from args and options + placeholder_name = create_placeholder_name(args, options) + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + + if selection: + cmds.parent(placeholder, selection[0]) + imprint(placeholder, options) # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes @@ -49,13 +53,42 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( placeholder, longName="parent", - hidden=True, dataType="string") + hidden=False, dataType="string") cmds.addAttr( placeholder, longName="index", - hidden=True, attributeType="short", + hidden=False, attributeType="short", defaultValue=-1) +def create_options(args): + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + return options + + +def create_placeholder_name(args, options): + placeholder_builder_type = [ + arg.read() for arg in args if 'builder_type' in str(arg) + ][0] + placeholder_family = options['family'] + placeholder_name = placeholder_builder_type.split('_') + placeholder_name.insert(1, placeholder_family) + + # add loader arguments if any + if options['loader_args']: + pos = 2 + loader_args = options['loader_args'].replace('\'', '\"') + loader_args = json.loads(loader_args) + values = [v for v in loader_args.values()] + for i in range(len(values)): + placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) + + return placeholder_name + + def update_placeholder(): placeholder = cmds.ls(selection=True) if len(placeholder) == 0: From aa88ee13c0d3b647dc9c11534ed4a742168b0e1d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:19:12 +0200 Subject: [PATCH 026/155] minor refactoring --- .../hosts/maya/api/lib_template_builder.py | 19 ++++++++++++++----- openpype/hosts/maya/api/menu.py | 11 +++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index ee78f19a3e..bec0f1fc66 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -52,12 +52,21 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, longName="parent", - hidden=False, dataType="string") + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) cmds.addAttr( - placeholder, longName="index", - hidden=False, attributeType="short", - defaultValue=-1) + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + parents = cmds.ls(selection[0], long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") def create_options(args): diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c66eeb449f..1337713561 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,13 +6,12 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import ( - BuildWorkfile, - # build_workfile_template - # update_workfile_template -) +from openpype.api import BuildWorkfile -from openpype.lib.build_template import build_workfile_template, update_workfile_template +from openpype.lib.build_template import ( + build_workfile_template, + update_workfile_template +) from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools From d8edf2b1aa9e83861bad1b8aef6da69cc6011de4 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:20:31 +0200 Subject: [PATCH 027/155] change load method since avalon doesn't exsist anymore --- openpype/lib/abstract_template_loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 628d0bd895..77ba04c4db 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -5,11 +5,10 @@ import traceback import six -import openpype from openpype.settings import get_project_settings from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, load from functools import reduce @@ -271,9 +270,10 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - return openpype.pipeline.load( + repre = load.get_representation_context(last_representation) + return load.load_with_repre_context( loaders_by_name[placeholder.loader], - last_representation['_id'], + repre, options=parse_loader_args(placeholder.data['loader_args'])) def load_succeed(self, placeholder, container): From bae9eef400e2b1195d8ece023543ca8d89c83b1b Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:53:49 +0200 Subject: [PATCH 028/155] fix update placeholder --- .../hosts/maya/api/lib_template_builder.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bec0f1fc66..2efc210d10 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,14 +69,6 @@ def create_placeholder(): cmds.setAttr(placeholder + '.parent', parents[0], type="string") -def create_options(args): - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() - return options - - def create_placeholder_name(args, options): placeholder_builder_type = [ arg.read() for arg in args if 'builder_type' in str(arg) @@ -112,12 +104,38 @@ def update_placeholder(): if not args: return # operation canceled + options = create_options(args) + + imprint(placeholder, options) + imprint_enum(placeholder, args) + + cmds.addAttr( + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) + cmds.addAttr( + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + selected = cmds.ls(selection=True, long=True) + selected = selected[0].split('|')[-2] + selected = cmds.ls(selected) + parents = cmds.ls(selected, long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") + + +def create_options(args): options = OrderedDict() for arg in args: if not type(arg) == qargparse.Separator: options[str(arg)] = arg._data.get("items") or arg.read() - imprint(placeholder, options) - imprint_enum(placeholder, args) + return options def imprint_enum(placeholder, args): From 349d57a4a8ec86364d64b02798c7579f6c3cb5c2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 10:40:44 +0200 Subject: [PATCH 029/155] change menu command for build and update workfile from template --- openpype/hosts/maya/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 1337713561..c0bad7092f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -166,12 +166,12 @@ def install(): cmds.menuItem( "Build Workfile from template", parent=builder_menu, - command=lambda *args: build_workfile_template() + command=build_workfile_template ) cmds.menuItem( "Update Workfile from template", parent=builder_menu, - command=lambda *args: update_workfile_template() + command=update_workfile_template ) cmds.setParent(MENU_NAME, menu=True) From e2506d569adf78c74ba6452643fec5d1afca0ab2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 12:22:06 +0200 Subject: [PATCH 030/155] get full name placeholder to avoid any conflict between two placeholders with same short name --- .../hosts/maya/api/lib_template_builder.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 2efc210d10..108988a676 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,33 +40,37 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + placeholder = cmds.spaceLocator(name=placeholder_name)[0] + + # get the long name of the placeholder (with the groups) + placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) - imprint(placeholder, options) + imprint(placeholder_full_name, options) + # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes - imprint_enum(placeholder, args) + imprint_enum(placeholder_full_name, args) # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, + placeholder_full_name, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( - placeholder, + placeholder_full_name, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") def create_placeholder_name(args, options): @@ -75,7 +79,10 @@ def create_placeholder_name(args, options): ][0] placeholder_family = options['family'] placeholder_name = placeholder_builder_type.split('_') - placeholder_name.insert(1, placeholder_family) + + # add famlily in any + if placeholder_family: + placeholder_name.insert(1, placeholder_family) # add loader arguments if any if options['loader_args']: @@ -85,9 +92,10 @@ def create_placeholder_name(args, options): values = [v for v in loader_args.values()] for i in range(len(values)): placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) - return placeholder_name + return placeholder_name.capitalize() def update_placeholder(): @@ -112,13 +120,13 @@ def update_placeholder(): cmds.addAttr( placeholder, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( placeholder, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) From a6d948aa93e2c84c001a109c50dede9b9c160321 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:46:24 +0200 Subject: [PATCH 031/155] add a log if no reprensation found for the current placeholder --- openpype/lib/abstract_template_loader.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 77ba04c4db..cd0416426c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -19,6 +19,10 @@ from openpype.lib.build_template_exceptions import ( TemplateNotFound ) +import logging + +log = logging.getLogger(__name__) + def update_representations(entities, entity): if entity['context']['subset'] not in entities: @@ -215,8 +219,15 @@ class AbstractTemplateLoader: current_asset, linked_assets ) - for representation in placeholder_representations: + if not placeholder_representations: + self.log.info( + "There's no representation for this placeholder: " + "{}".format(placeholder.data['node']) + ) + continue + + for representation in placeholder_representations: self.preload(placeholder, loaders_by_name, representation) if self.load_data_is_incorrect( From d6543bf281a418fe768059a58a1a9eb8257ef68f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:53:59 +0200 Subject: [PATCH 032/155] add debug logs for placeholders --- openpype/lib/abstract_template_loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index cd0416426c..159d5c8f6c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -213,7 +213,13 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() + self.log.debug("Placeholders found in template: {}".format( + [placeholder.data['node'] for placeholder in placeholders] + )) for placeholder in placeholders: + self.log.debug("Start to processing placeholder {}".format( + placeholder.data['node'] + )) placeholder_representations = self.get_placeholder_representations( placeholder, current_asset, From 4a02dd039de637398aa6c2a1d2c26ba772f720da Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 9 Jun 2022 14:51:48 +0200 Subject: [PATCH 033/155] set empty placholder parent at creation --- openpype/hosts/maya/api/lib_template_builder.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 108988a676..20f6f041fb 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,8 +69,7 @@ def create_placeholder(): defaultValue=-1 ) - parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', "", type="string") def create_placeholder_name(args, options): @@ -131,11 +130,11 @@ def update_placeholder(): defaultValue=-1 ) - selected = cmds.ls(selection=True, long=True) + """selected = cmds.ls(selection=True, long=True) selected = selected[0].split('|')[-2] selected = cmds.ls(selected) - parents = cmds.ls(selected, long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + parents = cmds.ls(selected, long=True)""" + cmds.setAttr(placeholder + '.parent', '', type="string") def create_options(args): From a97f5379b16f4141035629aa23d7ca26f16fdced Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:17:30 +0200 Subject: [PATCH 034/155] Add documentation --- website/docs/admin_hosts_maya.md | 50 ++++++++++++++++++ .../docs/assets/maya-create_placeholder.png | Bin 0 -> 31543 bytes website/docs/assets/maya-placeholder_new.png | Bin 0 -> 28008 bytes 3 files changed, 50 insertions(+) create mode 100644 website/docs/assets/maya-create_placeholder.png create mode 100644 website/docs/assets/maya-placeholder_new.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 93bf32798f..c55dcc1b36 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -120,3 +120,53 @@ raw json. You can configure path mapping using Maya `dirmap` command. This will add bi-directional mapping between list of paths specified in **Settings**. You can find it in **Settings -> Project Settings -> Maya -> Maya Directory Mapping** ![Dirmap settings](assets/maya-admin_dirmap_settings.png) + +## Templated Build Workfile + +Building a workfile using a template designed by users. Helping to assert homogeneous subsets hierarchy and imports. Template stored as file easy to define, change and customize for production needs. + + **1. Make a template** + +Make your template. Add families and everything needed for your tasks. Here is an example template for the modeling task using a placeholder to import a gauge. + +![Dirmap settings](assets/maya-workfile-outliner.png) + +If needed, you can add placeholders when the template needs to load some assets. **OpenPype > Template Builder > Create Placeholder** + +![create placeholder](assets/maya-create_placeholder.png) + +- **Configure placeholders** + +Fill in the necessary fields (the optional fields are regex filters) + +![new place holder](assets/maya-placeholder_new.png) + + + - Builder type: Wether the the placeholder should load current asset representations or linked assets representations + + - Representation: Representation that will be loaded (ex: ma, abc, png, etc...) + + - Family: Family of the representation to load (main, look, image, etc ...) + + - Loader: Placeholder loader name that will be used to load corresponding representations + + - Order: Priority for current placeholder loader (priority is lowest first, highet last) + +- **Save your template** + + + **2. Configure Template** + +- **Go to Studio settings > Project > Your DCC > Templated Build Settings** +- Add a profile for your task and enter path to your template +![Dirmap settings](assets/settings/template_build_workfile.png) + +**3. Build your workfile** + +- Open maya + +- Build your workfile + +![Dirmap settings](assets/maya-build_workfile_from_template.png) + + diff --git a/website/docs/assets/maya-create_placeholder.png b/website/docs/assets/maya-create_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..3f49fe2e2b801e89e2027ec521dbf700b97f80d2 GIT binary patch literal 31543 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 Pn$XtJKYU;9ROtTzYS3%b literal 0 HcmV?d00001 diff --git a/website/docs/assets/maya-placeholder_new.png b/website/docs/assets/maya-placeholder_new.png new file mode 100644 index 0000000000000000000000000000000000000000..106a5275cdb5bc0588c811d9a059ec3b6e072e20 GIT binary patch literal 28008 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 literal 0 HcmV?d00001 From e4a7dd6548eec1c3d85bdf133c4f273432313c12 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:27:00 +0200 Subject: [PATCH 035/155] add sreenshot --- .../maya-build_workfile_from_template.png | Bin 0 -> 20676 bytes .../assets/settings/template_build_workfile.png | Bin 0 -> 29814 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/assets/maya-build_workfile_from_template.png create mode 100644 website/docs/assets/settings/template_build_workfile.png diff --git a/website/docs/assets/maya-build_workfile_from_template.png b/website/docs/assets/maya-build_workfile_from_template.png new file mode 100644 index 0000000000000000000000000000000000000000..336b76f8aa1ef8d22aa8b912da1cc5f4827fc5e8 GIT binary patch literal 20676 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 Z{|`+)aTxx~sMG)e002ovPDHLkV1iGtk!t_| literal 0 HcmV?d00001 diff --git a/website/docs/assets/settings/template_build_workfile.png b/website/docs/assets/settings/template_build_workfile.png new file mode 100644 index 0000000000000000000000000000000000000000..7ef87861fe97322a3b23c28ee3644b76ce995e9d GIT binary patch literal 29814 zcmbTecRbbq-v@j|R>~nO*<^<#WIG{bmc94N$P5|DCWH_|2ub$dNwQb=-XVK$?w9ZH zx~}{BUHA3Rb)Uz#I^Uem`Hc7b^?I(?306{gd>xk@7lA-rmywouia?-c!>@PPSKx0F z1+gvR51bd$nvMv>wcyKNXnd{hxd;ReLPkPd#r5Or#7hq%=X24`?vAclYu;+h#N>A< z>2xd%LOjV3oZ2AS@`=f)vasK-e34Aq z#``I1YTcm;C*CkmQR;(D#^U=a-PV3_DMuaVBGg7L?`W|Rh*i9iK2kU!VMGx=!XSd? z1cBJc{(qkaMYS&odtOH%dM23t-Z}l)5%#==pRskSF6@becrAjt?uPd7ueVx8M~is7 zvO&&D>PW%evl>QdDwXlIZ@|xu|JUO-Rd>vb58AnPDoR7kzeg3>N%A#|&Dvw{W(s?s zj7^>I<$EtX353%7zIih{JL|GBY126BkD8fpji#rk*Nh!l*^A@USCE%4H9W#!4&$4o zk$K8Yw0-0eBkPY8aNRU;b8}lUm;cBgnYsKVj>EbA!Id6Wt&Y0NM?!3gcq#fUrkaDD zk)p2(;wD?>*?5w^1hfTC>C%sFv;)S7` z+fg93R}0r$9S;497pV$a?Ok1EHg}WCJWpKwdV&b(ub`vjQ;UrE#i5Ipry)@-s~bX- z+mXQxSGG~fSG?1dZhLp-hFy2)%25`*z4mP4GpW+&4j=FQZ}-xlF06j{rt1h3!;mB# z3ijMcozHY3bNs>Yo-QxnAHiMc!=^>=j2V2c`?J5Vu{NK0Ag!JRZ{0WHz9kbi zGBTo`uSY5BRU^kZ$FIN|!^_J{CFsJ=&+iKVptQ8qLqwmM$m?XSJe1^a)r%RFxHuZh zVr#ak+;$=)EX<~OUd>B#eSQ7DdDlFDc-of~ujA}5U!-Y}9m&Glbdj5`HE-X(eLj>o z+!^77k>964k zj5T#)f)p|NuXQgB@>+#O>V-FzlG9371!rIJXB<_uQa*g)o#kG7ZX)$S{L>wJj}WT| zrB;tl>ZzLTzm1fOVb4&Xot-^Gq3VwRviT{YP(KO`8?CIYHWv!n$NKx7mVe(V)HwZ{ zlRsJgGQ#kK!l{|CkkI;6yu__1&u)@}+eBJi+(()LHfCjIrSY$~ zmc{c43FM*rk;`;hiYZ=ue+O1qtzW)8Fl~?Z-0gjQ@7_Jun7=)#kBB5|_eY<@7CDqA zC5`hsJ6;gnbs&v6K}LChrkjMg1Jp%$rOx?ni#zJM zTBgU53^L(!$9pS_v$I9kBiw%Pl$iUModyO5*xP5nv9qwu@GIo1tvCq;OQfl=3T`Df z1zXzNV2IQBeal%I%1=p8H&#+YigYwJ`Oj5g;Zsfbsj|jMGtBbm)zs+Z57GOzwY9x@ z^Tu|(;-29T3?DHh2}x{h>@P04SF*4#$`04!XSO9s#M?U)ep7%(8$_olq zOS_~_?VTVSEZMM=MG!`<61up*_#O|0y}WCZtcq&KJrN`O>P+6a4XeXE3+Asdv(4YSO0?I zai5c#+FC&#F;P&aJFytEmOtY)ivU$m~uL zwVNbfT3_$tdYjD>@@;)eGxh;_*sWW)W@ct0BO|jElC1V;bDxxbFJiyp>5=_s!O^Va zfX0t3;Ap#@)6>HP6OU{Yo_QZF#5p@V^N*k{-IS)V3o?!zq23L_)J^)uCGs;yf}+`; z<3|55&7wZ8)x+>PjjqRx(;5n_^8`W9mfzz4eL!n_gQ7k7XnszdgQC5V9sX-}fG2q8 zqHxJ-noqqgP1}4X!!eo%pW_`eQ!i8Fa^?>CRRr_LKBC~`_!*-}w}R3-a_^knaiCCt z`jn7@LQa_oWwE=|Tk+60!bFpoh6X`9KRb(xGI{jqzVO>MWi_=-CFV@I_#bmyNR+Lu zZ8W32z{_==VpSqi(m;QIT0iVJK@kz8xc>Z69r^Kd{P_6zk)zdiByRF%BPjn!S$2eWpk^06xHhYc!&2Yn?3k%SX%xus!OX-^F+MIVq=}T%VJ% zv9W_gQA$b*i;*8{=Ho+09bH|;WFbp}BuWQ(9aFOFY>c{IgRzc6l7Om)Mo@lRTF7la zTLuOOcFiI`hSu?Mog{@JYu}u-G^6eLR@f9g@>6x5iF5q^DDgfgE32<8^`q>SG5WpG zU}op$($mvfLIfPk zjT^K)FJGAB+8y8NWhC{zt5xLe2I%cPROIOKB4J}9Za;#Tr+!F<|crs zy>5GOb}TkXx6Qx9BXJlo%!>8OXVOJD0yC5(B|F!Aa;bPYpa9*oC4cT@vrq4|$M<<9 z+F`SDPwgFhniK0ELN%j!B0IkFV@U5$_^JHiY@$b+iu#yKPoULIS_q%h;ELPrujpXH zy%T;#`6=JR%Ie2$5=9}zlD2e(AqJ(X2*v0(`kxNyuP5>Ta9mC5mp#>_Ko{f0SQF_n zv@xSNO}$b}Z9=`owew1ExB0WLsKTfG*RS+?Zjo#S((H-p?>w)z`~6ww(0urH7%$)G zjt6=ga@RpZPVTK_(_t(S`yDUFH+%v|*L?7RI;{xrM5E~;iQ74L_*Y2REbMk_&V(n@z;aht& zN;9wWu~h`=N(5rbMtVi218oUITA-)=ookX5Vzj%)6H8vG=kb=4KvIK2fnMFU*Jiiy zFvpt%&xcFRh5ue{;3Dk_RhG?&N@G@6A4;wx?BV3kw>V z-&MZ&XN5!f5BPfUUr+j;_NAQn_YEPA$t;$99wXk`U?!oJSAEL&t;7RV9`slBV$TK2 z%gdL|6OY2e!eU}#pq+XYQc+Q{x3dz31ajhj@WrbBw2|h1e8p>VW`;IW8W$JWB7srn z_t4PL*jV+qZ>uv;pvQR~q7aCPD2`T(larG%eSK)KsUlup zoO=csCJ3|r15Eso?M3)hLf=nt-fZsd%7~}s>Z}(;e9ABItX8Cp#PoW5)@zWcoPX`u#Qc?sfm?C^QAdajz_!-q53 zBJ&Op>q_DMg{bgp86u@$M9VV}YGN3vsuHgcHHx;Ga6j{LO{Brr*Vorkc*R=!sc#wM z3L3}M&=6_nSIqCp%7QP~f4+4>ylIVomL_ zbSx|^%*-;6gtJkp24sx<{AA{7Qt1tpj)3Ru>gs4|Y2g+F&_DV3v+C&z-`eI*^!M*# zr{5lBJRRB8Mn@cz7|pjt!UB|||I+EE0~kq%gOin&dun-Z?wPVOT6u=2f{aY?(MiqV z#{Q3!noo*Lg$DxtRu3?!5udW0nupC|WKBJNM|ygCuB)OW#EPdq^$@Rx->jk|hUsN< z)f(s1KRxQVDm|jVf$^N!M+AG+6M=Xo@lZfu)FheNK$L=#((vuIr4{R)k3&;aQ;Z9* zC-Xml{+z{$hPYiIzaJADJNlUdfmov2{D$sCCbWWxK)4Q~AqpOUwxkn6APT4%#y77Z zR*~a7@H>GCHR3y!_&yq9RXMK1@D35~Vpo!&$Ki|g-Y)}I5!9kyC;MyRBy=R90Q=V` zs)vZxu@ME1DoRR9rluJ-*E|6N<$V1*>ArF|A@l6?R4rdGSB3T3wQGVZ(gp_8(+$3~ zK&*9rSXo(%*3$L7y}j-1HX=mN$?6$T;fDv4uks5E?T>etfhBo)dA&MVAF$xLM#{k; z6P5M_&T4ga6|U3K&f@JbDIc){?~C)0kP!1^Hz@|ffx*FK!I!qcwm3LAgs39Mz6T$qwmt{7Kw1`vHjKpPNbUmV@)$^ah2vs)Cnqt& zpuA;s^Tk-~AzbXVw6u&TaUl|PeoA6u-`VnOW`Qqx$-!lJa^gX?G&Pwvyt#@^8!xA( zt1FNE`9(RMIRu4mPZ7wsy}iBJz#zk+RnpSXAkIwW?&da=c6xSZf4{E;4S}$kCBQHo zI~VcmNfLZ{e6Ue((i#Qb-Ot~jQplA{R8*9Q=Y8|s!u&jqACzUlK!CX(M^Je#w8i}W zYvtE7Ug;Qk{q}x4XZ>5Udv7iw)%ZxUxuBq6ijbQ_&nNL`nR3CQp=_+Ifah^<-0)hj zT;$NJ2|^wp)L)$SDT>|-lj{5R=#*D%>{C6y5IOd#McR`gMy!aas3^i*wQG(VK6N*2 z3I9v4G03IsF)UCcaK#X-{@gvMLpi*DV{P2fcO+pPz8c^<^$=X*QBK5+hx5Ivk`(kz`3!nVy zOcXsoG)~+Kj0R-nyk0Lu(F{zJRL@nMF*<%@s=_{9gT2g9vXeW7^sBJ-1t)Zt)18zH zkG0Yv0bP`n(=NyXE^DKvjX75)?4U+I3cMlg{Ff-A`O)Pn(w4)f4S{x9Wk2_;#IjFF zNQgtf?v#soMriwEIXQvEz)XZv+=I5NRJ!mvvH(loLnMkjel6sSxs{a` zK6k8XZB30?cQPzc;|SpZBzJ^p_#C@Kvjq=sqtruL9dg^#;Yt-cY*fb6%i1)!Wj+?7 zMOd(HypnVv>RCH-_M-b*m0zz#4QN*{|9Zn)0*P@$=l0*^5VC+<_lzWp8v>DlxdgmU4}thN3s5$4VbfAY4E?^#B@+ce zL0Un9>VXvzszvCWEWrN7iyo`eOh}T-JOfO6yu^uy%Ac?fO<_LN}_Sx>%*5%5@xlIOj|0jtC2Lc5?dm<%{dZ*}=tDgg3_3s{kZInr1=U z^xSN~x;UA-a5pSxwh*O?C^u;(%vKrg679qg4{Dl)1?a>h?$w!o^jyx(4@c<4ty4d= znn+N^AVS>_xZhx3U!VI0C=ZGs`Nfsr14O~b@DQ2m?;Dl1#1Nl8thS%SoZJ~Uj<{U7 zaO{`ZP?3Rs8#`J`F*YnAuWy+>S*yfrf&cK=+grW|P+=Ztt0degP?aqEO*sPf{bO{r zZz2MOBp)9i)|j@22E)#T2l;i!UvqMDB}R<7YnRq6e){_T0}JyG_T;B=c^Px-7Mq<&E(7S@1MRP}?o+ zm+f7-J8K6@7W6sS>JgzI0`Fx98{$5G^Z>dJA1j-!LWp6Q6Jq_vTc~M^Fg}<BPKIl?k0U%*^s8kcNYi-&sBRD0d76DG z=|wTvs779Qe(bF;F5YmzTj)^~)E|&UG&ME5y1D=!vA2T)gn@yfM5L;#yEZf9bD|GN zvL)}Q!=aw7qLn*XLK`FNCZy-->B*w@lI?Lgv>uP2uV_?ud-t3mLDuxpYy588k+M6yiGOYl2o;`a8RJ@x@PE~aPX!ysZ zB=tOc)$6C+E20;&D%Xfb7DywZ3S_GYv+ z1Bh;`A?=uy3umj^3=wqFR1I_SykA}H#tRJ5Q@WdLw+APlf_PBE5n?*Bf672dBN!JrxImTL_h%10qLZV{15BO1U8ME()B_YX%k z2?l%ZJ?_#5;N3zg69Lg$Ga(=QHnieNQfHIo*)?<^pCU5R(2yFlT5Nj|$*>FxsalS9 zc2#=A!9tvVlrN)fpaPFdjMI{jb>JKPm(Hz5X3lA8u~sjgoOFszaJmHjdZy0z%ZKz{ zbqZBF2~2?Klc!(bHMcc-{}-~J*~q?LPRS!8p3Q%9?XQ)E+`}<ZS&oOm&3>a@ z@%faN_M@Qz4J$=2pK0MtfZSs(yG2bJ&ZxTThJpHDa`ME41OfNhhzLahhEGylI1WDs zC~#5IY_2Xo^bUh|BOD_@>XDQqo!*CodR)=1vs~;E+P!^u{l0n#opC8_83 zOp0FHtxWOp@vs3cEiDKAE2^qQPF9Oy!@P8Mwox3_;`u3YK4TjlTXO$gC4T7rP_!1c zx7pt}Jmg`$y}ifVr^oG6rJ4>*tVK9awVm>FRuC>3(oJA-IPbFOs*SDc+nIz;H~J;} zz`1|+o{gfX?)&SI%m6eNAX4g?_W2a*xYj}0~ zY`?R$Z{d%dNv`44@v_bGceQYB`^8}-$13U)px#G^p1e0r-ci5FA>v+}Jjd@NrlhGU z)3*!gW@F-cW`+acGWy5iC6kTAvP0U5TSJx8jt&m6um38YI-76OeyAoIrg*%4YLt~E z>aCa6SGwq$s?<_s&6e)sCX_5jqx5k&`)(Np?{MmLU#tmpxOZEAP!xJ%UEA zsFqk}B%1R>@~Mc)rXl~i z$l30*rtJLj6py+Q$7<0-B&b_-;z(-*WNL~mnGohjC@HS3yn<`hJG#(sl1>q=m*NerG(Pr=#1H*Ll3i z)|jnwu;WuvQNe*LvJ>*x`{cQ0&h#T}IJ9p$1;g2GHE#sbh|SMa+%{?boDPbKvg#e) zy1w1`J5c#D+WvGvUtoMc6+8H}u_=`vic)6C)2C0pPPdxPL`&CCo|)?9)W0Z5v~!jp ztynaD>zx9W%$jVo|!_IRtAI*B@Ek%u{N#F&cvP>w&m*buJA zp@&yrZF{(3#MXp|oM2cfu?OEqX8+FJq>Oy#7jMbJIGi^Bc9t-mzj(AtyYnS!D~m;X zEt`Z|>fv>&tnNCq!t$l@^{=lM136Bzo-biX_EQpI823DQZ#FS;=F-}Po@<^WK{rxn z18tue6+{yR)vET@0q;pv-{l4{YQo(SUKz^2fR*;+#}A7HkyrcQnzrEKA#yQXXq$_UXq4q~j9V9K`Qr)+ZBfh! z#U_2|4gE9fQYvp=9JutkKlL>=Z=L7vuUb35XQmk!I6-CBeem2(zm`adL@`7tDtSod zi(@~mIQn%yb|%NGAW6ur2z?4M%=VA^aQ9a_O(Lc~ zsj7rBQSS4v0!0&UDsLFLNg^4h2F5hh4exQ5JWMtJBq?gJSVH8@iw6hN`3vA<8@?#0 z?B!LACY=!}+8e~J^XWsYQbA(ag>MP4sKwviTiM59;8^2KnyYyG7LS^mI*LIQN=nU6 zVKddM^P|PBtYY(SJ#fi0uA`0&1nNHR@OQz&Wu(Vs-S+P%T;h4!tvyL^1* zsyS-4u#rPaIlY4Hu2T^($jqv6he?&WZ8|GU5=r79`&<_+j(of=aQl9y((|~LOL(Pr zc73JW#U(ttZXsK35Y1_Nw$%8i!Tf8>Z13X@9Ju;-2!j)na0XwRpbkGi7 zy~f#RKmwm;tL18eFzB)}cvpywj)@6NaVTAdm6C!2VlUiJ?)WK@UezB`^}G2_?Y#ru zv!e-Kw;xZEcP4u#x(C&XX#M;q#nWEDuu%?}?HDRM)5|=3mZ87?JKzJ$c~o??&C|%- zbMzjq{mAzQyMOZ|>M8t@76@1kq{@LD^(V5jr>CbLo}P(! zN2aFgzkPdas<{#tSZUD1C{&zhsr85VWOHX}I>K;N}Zu-x5b^yvnR0n*0T1-w}X-s#L~O6Mf><8Q#6^qs(n@jLg;PT zh8yi6*dNKFCe%5qWfyX4eLlWuh)?QAjl!HA%~*(no3$1d5P5RC-6HZ|iu8Yb3gUEm z-5XW}$)!5ILa|h?&dp&mWr zvmP$+JlT)^LyzzbO;4w<>ArSc#bJ2>BJQuBry>Up;Aj!yN~)`1AP`A?`#P9OAn&WN zsu~y=*xLgv2PYadb`*9bL#6AMk)53#RRmbxQ6D}8AxG;xPvY;J3keI?*4EyRrxy%lhDy3;*iwFg!t5b>$uVGSjv>6w{A$3=xoojOSDL@}_0fW*?)WoxGT z41ut~dEmM^-QTY=Mk0&+2@K>{IHk6Zju7BDC}gMnO})K^VDv&Z1v~ZSq#IUXx->&u zSC3hqN8gK~En6N2QzN`8HKTs4rTR(SpP z&WwiiK$=>etnc|p^_fHR25iM%#Ixd06{M-L{9 zI%U#_NzH{*3cjGZf)IP`JVO=-ukVCv3dk{2_ zlHUvL8@GA!sAE%7s3|GKkPi0tngs@EC~4N1s)`C4KR;AzTwL4{CIX?c^kemjqM|w3 zAK?F)!hpBM#l_+CDEVy1VG%5LCI&9H@xkH&6~5N6F<>$I71(E%AXkcrh?sXLd#>b` z>%CelZEKSq-+dw{NAb{MzRGz;&+BO6+c$PUB^{md{_JN2C^R%Q@AI9MPCf+U`w!~D zm8Fli8u|3Ry!-oWV_LZ``>P|EzB3@V>FUy3;CvfGqq?p;FlDZ7ROs3ma2&hxJ^*Xj3ov2z%MF*t7<2@c>V$B_wPyQ^ zqi%0_8-2*-%+H$y81A~C!5t<*k+eHOW748Q!V+V;f8VkHi*jLMA*f2=nGOvd09Tvv z82K*pP^x=cW`R<`Uro(o{o~Iux=3lp=tzd$uLroSF^~-GtGPRo!dGRoRBSel;AE8WrSle&&Q-H~Fo)xSL2jQu$xzEQ~fmt`zOm0wpk z>7G7Thh2a(K4EA(_bW`;;f~F3brRmsDOq%wxQ{ztyojUs(~u0i$CUoZD9vfwHBwz# zQJ-dstPNyvj4NX#tc1a3)+2Aqp%!xJR{537M9uFUv8jFA+}<9o_pU$6&9RIR&&T%G}#o~<_Mc?P8! zY%2)nfWS5G?oBO@Jp9u;`|FLVW_oW+5qq;eDwT_Y!KCdy3uvQDQd3*258D}>#%=?yZ<@t_I^yqxh`LdKaSqGBt-am6GvN*ryAGT*-um?AWOacV zH$S9#|A3C1)1mQZys_2SlE9l2b0kq$K)#$i%4csYBq1dos`vJ;)r;_E=j5z+S|$V+ zAQfzs>r_Hy7r)`4^K`4F`kbVtTW%@}rK;uFD=LG02ij+%KE{V)*Fo`ZA?rcf31{%a z^u5pafKH$`_b}lq!Uj$fjJuRR`Apm1*PkwMV$;^@rNqY{0o!IF3fU(u0>t^|&6Sli zP)f41vjvjEO-JVE=i$S_;s)P3HV)PXXl^3V#}ssmtCl#3?-gVKmE4+7R{1IBY-sfa zi-+ybojZ_^ymxQwyAL{Dq{Z}iAM8|E<$ivCQ1472zXb9j#F5|?hllW#fsqk%oT3n& z<=_Z!o^zh9Mnu;Ys`Y{`16iq&K6Ny&jHlTGp6Lc$WKdT?b2qMgmerR8HYpspjXAAA zQr^fJcYH_x;#f})mqicFj<9S7A^zPZW?j`tE-!(&lun)MAmnV=t;f6gS@pwG;Yrd` zH&WrrndJjfc3E$_Ru&Jr;yYkkusqA07D)l?GE`g(TzJ<_F^;%7;xE7&ZhIIj^zFh* zd3)QPBqMvkYFwJ<7XJ%4IrInQ^59p?F$zNGg#T^)@q-M)OZ%`E1d?nAziC20ym8|O zG{%?f;~@S!xVV6+41sH#1^Pk_AfAG4yYA{lj z_^tZC0Ft_e7hu_!IiqcvsuWahYHVagM@I(+b7RprwVYbWxB{fayg}i5xM_%m6$l>k z=giEq?a1ipXYTIq5GN6%(Wr7_F`+KdhxYr&Tl7`L@v(d5=@5Y%7!;6cglJel_1gOS zc>z6Ihad+|6a`;=trN+`&q6#pEsEZ!YHGYtVlTsS=VvFbL2M|C;Q~WYDj8&C8KU05 ze-EuPSi*>$WbV%&UYk)KzjtCZ!==wjIWY3&?vjMUmdjAff!x4m1AIIzgvWYtO!@ho zs4tKP_}ib&-VO%^PbXFhWL^?l+tXK+ZKDl%-B_uB&_weYE*sem!lF`Kx zZ?+zPYQ)5+dfWW*WHy0hJ1SO``Fmo*C%_j z9SJ2B+02pBM#jc~Azz&y+V2n;XEuII{C?My2a?eN0Rd2xVOP0S)lOX#uf})!5FLHf z_O;U$un!g!rjEdRl8I(4Eyg(g3HEQhNfAipOz^mK=M3@;x}{dC&dz(weOUmwUn8p= z7jM)Fj+H(i0tgDe4E#u4-*X`NIf)zQEeCFixh$o#MtQ;Pg?t*nmLPGHpEQ2&?lC>0 z*%*3?1Mq>rfJ(@l9N+Rb#eOp z3Fk~09Fk7Oi-yy5;B9>teO&|~RO^g(Lwm7*8=98TCHpd5d-y31+$``leL7NsU4{DUOzUG&XGf5#w zALrA14^$J313zNDeGq12+kGgK2vCBPogMB1fbcX+fjf6H#=fqzu`hCS*?GMrw-@_Sn8TFTbNbM{xv#h@O0~7S-gTp735D5G0)ERa*KmK znrGGc_d89$e_0`*bG_SP3Q2m;W}~&jh3=3pYxCaS>jq_mJw1p% zjXkY*o`{klHMI}j%|ldj#$lG)puO+n;x4hU#^BnC?W6V3_2G3)_dcqus&ZL+;DCTs zu1wTVVb4tOvi_~1m?yXK;uS z|JpU}T6gDS>bqJcm!9gyX=hsu8v%;ibC&>R0dN4MI^Gi)Q4%ybF{cD*010r{rz1f@ z*bu~Hihtqa0ztkcuR>r;r-xfWA`-ea}ilLc+|9vDyAn0G^DrbkE8jUgPRSb$MQ1R|%$@g9A4Q0~gneks?z#4rOp4 z*_=J0R{}=kIwK<{j(PV^tdFO?2#yyFY3&k=NFYAqNMd4QSifHH%~FBT~w6oZKaelJ#2xz;_s2`GC(B)ftdb*_rKH_^9oUlf0XKO1C zKK{YxW=mTeIMo_u*0)g1bab;Vk@S<3lTgp|72?5-eE05k#~{sPZ0KHjdbRF=DhBE8 zF5yAg?Z9$~s<4y%-P_rTmdyYIoD~(+JlD&UtHi9rI`CCB2N(#TxT_6V2(IO*!cMp? z{3lRkZ>RTaXlo0?UMkhHsWo`B%-COo9jU`n4{q&ymS-U9+_hDP%z$C{RBozjC-}i@akyx=g&VUCMYK@ z(LE_^hAv}&u+tE)Fh)S$`9D1^OKFq^8E334_v=?&S_!0|UjD`Ym|byEk$`$aeZ6QS zm#YXFyMVwk9IBnQHP6~eiDh(yfon^9S(!y|8h}X%5MFKy$XEmi2WM86ArPKgaUD}x zean+>i`_uhg0%a^pt%t!()(fXbK^Q z@@-*yMI0eU{rk>{c^=#p+WFPh@&5i2(1S=xNvR^@5)$0^S0P@v4V^)m2%?PnI+YIR zr*8iy^l+1h0f4r#wuZqPSKM#0AJk5z<>Yi~Tno#~`68OxSXgA_QY2-@ICS{yhabux2;Ou zC#sx}j*gyY_cu2$LidG40S*q1CTwJ>^h#(WaL0ynwVM}r?$&vco))1Y)&>?v_(1T0 z(1Z%>+}s>&`}*_a=TQt(Zi`7UCI&c5T~&2$sy-#{3xw-v{9@TP9ae`6q4CAb?Zch9 zl+A%t1Aw&@hU`=(vFOK-A0c-M7{nLpzL@k9ruA+GK(QME$)=I(lB~ld&}H^gGL+={ zEpCQ~4@ZoGBQBE!Bim3uU^BxS4M7ryHjS0p^jVbx5$p$%OEE>HlM637PKhI~e(Tq5 zII+v**;bO5hlfzIr#nzF68)*Pd)E+43c69A5I)XFGQ`M2dP@YtWwEAEem*~C(W{C} zW5&mifOHKgq#_rmI|u6%g5KvsDCU19CHdJkVqtcvGnSfwpwP6Pq$ao5;v6zYaKDtY zRRF6$AN+QY3=cBhguxQ&y|5#vy#z|N&ZZmC{gj?Pi}v;P-6NzXA(l@Pc)-XQ&^%XJ zQSlKX98mhPY5h@!VA~{QE;t-bRKMJctZ*9}9tNHtgxucS1F7T@Hr&1Euq6+3G(9|_ z!MH1_?;Tuv5ZI~kGGIOGDk?O7776Dwfz;48jlJgeFOG)_3JPG^!(_;%aALu;K3w=H zy|+ZqtN6^5zfvlUoS2BH1Y&2grZ904D$2cK#ba|5qj!~%`VU|nPguL-PcQ35vpt;E zH4^q=*ic%z$sp+~F_+|BAq~Gd?jcUNl8=vAV$l`+1r$y);tw!qM(D?DLR90#|BHQ+QkM&hyJD89dzwuD;V zXZs$M2r*KhJ%K6-6B84NDglv@koXy`C{k%NR+d~xmk2-mo}Su1=D8niIzp=NLBPiZ zy7H@I2LvLP=A=pjjudpQSI3~L-+l9c*6Y6H=M5?A>qoTb=jKinn@6BhmvoL&p!tUo zvwb@VZJK@e?i~~e|9}961fFH@ixd6DHY~)hf~Qw)IB;K~osRpw`gQaG9w1W+mjQrY zWAVEy2&y}muh`vn5}K-QsstziNp@I8pDaOmqY!Xn=H#@4$!91+5R@?d08n;tU|9csfth}NkCYYo(RD0Yd$e0T1c!as-803jNr2T1D4g36#m=jY() zc$1KD^dbzzokZawqxc&LG5ubqp_P`_)&@!sAO-y^d#*Q3Rvhi7>XMR@D2a(5*o>Ae zEm?%R#9Q9E9%=)L%|E0(lv9-oatN`Am9Z7)OWgxRVE7Ph+7(AAz%o&=9RK*BV?pu{ z%BaaDCU}JH9~ekVLXyzd3+>~N)g>Ib{-D|Z&vW|{2M)w1&cE*+1_IwvJfycX!I^Uc`nofQ$c@+5f*hg?zs$C@-IY9NxS%mD=0d_SYvuv*L@m zfXair;oD$PcOTp-h}=d=Gf*^}nw!Ju-j@-U+`)YPi;0(04IZqB*Ik94`^rTAOlsNr z`ApbGkSYd|2qG|?&Ksy7%W<+ zcwX@lqd`t*3-X79#{TmpJuMA@p+$n3rVGGC^efV$-~KZTfOP{84^IaQHYD|Klap`W zgCz{;06Mieb}!Evgpp>C-f3%@qto)hP%w zoBW~oc?}HHBqol*B75@W36Qpb_lip6;)Y-+NJqgM()T(t1%?O65(0A|%m5ODe+A-w zr?(~Iwe7E!srq`UbOK!eBo0W0HJm<9@0IS`O*!Aqf|(eIyABTrwR`Uks9&6IcN78e)8s3pjoaBE)8 zw-5-aPXpwz8?PX^b;}JDF)-7QkB)$;0ZFadZexS_2M8qvDKYQv?9j__b8xIT;)~X} ztZAvJyble%mYg+d3$hC&chxd;-aPy!meO!3?$XlR(2%nNSRI0B&k`js_X z-j1Lmh`(3a_Z#IKFjd}GdSkh`LDqmDs{oXaXQ&xD? z6evR?6B7MlNcesO;6hNmK-{yGg>DWfM1TUeFNQ6j$O}FCIanP3F=%9c+I|JGi!o*n z3<;Vac^D(i(8J^IT^o@0fI>ZECW6GI&Dl82Q$Ul3!2L62*rGgkWND&#xxMUt!rAco zhwCt11Or*3&@4#VH3}^I7)!N;J&&En4`GU#L1yQ-Ev!u_x;M_r5s2D^wdM*5DXB~4 zC-crAka}2kP$wWg3U(C|1@bK1v@eyV{BULOLG^(-3_ql#GVE-~ias~ZeG3y{V6seq zDbkZ_G^)LEiIrVDTjBUXU}-VohWMgd4(J~NHz-3$mjD)l1wn(d==g9C*k@aJH#Lt1 zsA_OI;<@iIGq?PyzlRVzDE6%8q^F;Qr321IL z|OAK^1gioO=Ysc@DXs?r%!)J-Ffo#*|P)ij-VYt+k!)Y zFI%Xpt2+lU3~?DgGQ3Nkn1O>>T3LNw8{RE)Vhu1~*pt}v_CIdfrXCg;$|LIg;_3K^ z*YGC{nqdB~EXE|UuoIX!FALVLmuoB~;#gqJBxU)!4$MH^gh9&x<;y+(KSr8$y+Wb{ z-4W2?$6@3Tprye!b#rw!F*D<>Zu#|?+S3VOGMLPmSXgs+{0aaAg7`X7=Q&og2vS36 zL`3sdXx2FSK9_GXRpp!$bM0nm6X>BJCS_7zz>vf#`~a_!#!u~=1_22Pe8Y#Z-@EAO zJ{Z+;g%+$vf{p_+MU~SsWB}wZ4fBhQi}MJER`?ibAFxfeC^n_vIFAA@c`|4TvvvOp z_jE5_fa(XDBrx(q5o?IBuW{}`kt-wkvs zFzJ@LFiS^I4{^vBjAi<;fnhiZI*%iyk^PW*I2ec?CMTKwcx;R-;I-@PD@M&TrFaxM zEXexx7)J>6f~*d-z0C;o2-wKarY2Q&^*LfjZD@|M@7@&vl5$=doRJuVN(%GM5Uap` z^X1DIelR5M=x2&RKL{JbMQ*wR1x2kH42D^*1b8b#fXi$)}qT=Ju zyKlhW-v>o8iXr~v$G6vTpcjC(by@Eq+x?HnxH;2Uf3hyvSE~8#hn}|f^G1CZ#COJj zrMw}dgZ<`R`vO!!u-AC?_X@#ubcLz_0i#&89M2hl(oubgM*(1Y_+|p&xFds#|307qT2&~zTAq$&mE;*@ z2Dqe6~8awjA<(a9mtmJ?;cfQ4BC@ zyR{4_nWLWn>-qKaT}iloqs6!|PipSGCY%q$GtklfaBg-K=V$PKT7LMb7|R3B?9Z9! zpQPvf`eXqkz@C5>gBo80@pZEy@kM?PXu@=KKib=KAXd{ncUQZNT}X&xYO~BqIP(;~ z(gP?_BDblmi;K$>MadU$%PRllf0|T+VOek*M z8VRlfmBh%%2wXQ9_=5332=F`%Z)^`sY;S4#z-5RsG^A~|=aC;891JIafNQSL7W&B; zF2uiNZ0M&}%iIaN{*;KGF(*~;saG4dfk32+K$*^G^a4l&=`+}ZkSAZEeE?r50+v3g z{bn{cHn3V%Sl28vKnsA1OwMOZA>SvP!7VI2(bwnrFAN6>hIh*aySuv!?Qt6u)ub06 zVqzY^=-(-sR{l_V33gQ_@K0#Ug(FNu*r{gfh5+<=_e*&ny1Ev?Z#>^$QU3oDZ~oDI zOior|Bq7CPTZT#DbhWtK+W9)LfAAK{Q~|s^RvhY@8hQr${YYONl*)lBmJ=`_2#AR# z`<#G8fukhfH~4BZ686j5XzAtEx+}>L-Kyt0bM+rx-V1aNXb1p4pjW=mgfC!v0qq~; zh@BXL%lc;g3opIb6}}STpXiNf9dyG_pT0LX8pB?M=G5NZ9VeRsnTtze=&%s-&vy*b z%0v-K0uyn-cknxp{b7o&!KslYwkCZ>c0&OyUWR0 zKJ2(PD|y*r^8wEQeln=TOw7qyf%GG^ZTPB}O^`J8K?nv~52+;Jh>`6JAi2R1`5!iC zegT2mn@vVrKzpaWPHZ5%bG(ud;{&^6$5i0rz)Jzb|AH9%|E9X&HNrf~DIAZTX{#(k zEM@RNCyW}4*ip=Ue0}X)12w z|MF+Dzt+HvNk8krPsM5M5yk9q>n7(8Rh%*593i#hxYAAHxnTG0N&m$Ok z+}Yj+7NrMPH-J{i9VEQ%_c0{P)qmer6oDp|)dveKG)mgch-rfHiq8^`QxiF9umm@J zu@&nT0%9!Rq$>yr)4;c3fkO`8v}8o`^2LiUd3l(xA&&HvmLH0Iz}z7OFTupu2D8uQ z^Np_t|MZfwqM{#M)=*~uB&cg>0Kk?0f7<%)a4g&S?~6wWaaZUrM3Rck2H6!tnj}d^ zWXs+oBavC6jHGOtnc1Y0P)KG**@UtR;r)1?_mAIuyg$eB9QAaR`*Ppsb)MhPya;Q1 z`{1A;107ojhvbt1%{16?u+?!a*}DqxP`0oMcX&(k>(qgwP9MZ}f{b5(g_{_tF+E`b zT+B>xWJjEQ-1PoE?dHwVM@(@nJWh)M;)IlJj9DFn6`(f=PjK#aG@S0uot^Iof5D1< z8dn7)EO8;@3&?D+S1z+d$_fg8C1fth`1s&)3=YgKVQK2?XyxkFfItD)I_8VJ2R3=| zi%#$CChLV)NMDi<{gAhMUb}}BcOt`eZ)8bW-DE&;Fvy-ZlOx!gQZzC)Qb}cH3Ka~F zRU&~MC-jf&oGvb4Z|N8A_}rNM``K@gIPZ{6dH z<_PtU2)BSCrk3>PxaOgKMiTk_i#AgmwyL`drwgbYIrF&u3Mh+`M)#07Xax*MmY=rl zGqQRkoEc`$nNUb}*t>Xed+Fhpob~&ydrCPTzNk}uzAVrx^F~*bBxCpSnV*QzuAkfz zEJjR6Xs#`csU>*?7)NcFc}P?6xvo+rp8LxwdkV!;BgT}8i*-sXBqEDh#`)sfn0=B{ zz*C}^X?(3->e*qgsZ-?IN5>r{+_u%4cs2eoR3dGSqsljbxjw%unXJ4AFGpV>|EIB{ z*C6%TW|I7V{aFPadV20Ere6&7|6EH{i&lL%Y zmNWY2K05^VcXW1cpuf;UWad6HzB~-AA7JHSw=q>%U95zISwmsYr|L(ta@m{-=bN`o zuIRXqC%<{~1|b`w8YVTy-BRKfox%^4428*X#Ayr4dj1s;t2?8lgjGLeE$%eaclP{w zCR*Bn(9o2k3CvjHF0(xTwb3F5iY_iCv9Z^#UyoDgTbQ3`4tk5k6v!5^FYxhoOfFEJ zV3&4Pe|`a|egj@WmH-;X%7y%dWzQG+;^j-cTbP-k|FP10^u@-yyvIVCrAvao&pnEWFuZva z^sPg8fhp!FI8S(KZ&Z6xKz{Emu`|}wdxmZ*>xxh_m#_oo0YF*FPpn*A8& zEvOFnm3Yu`C*`(iaqe6wNYIwV(^Y@I2jNqE_vTG*542-+DnS8CbKV<&du<7HVa?z# z%+*<$nXEV%!Q|oMZWhf2-D4sua7ZC~L5*h%g|h_6d46l$6jx%&e4NSfu6U{L zTHSel;1O*U-SP=ed&v=*J~^&s9@VNG{&(dW8d-FkH*dWs535{->%xnmpfk?S;>3Z- z4{-Bk>QqSZ^Pl01G%_?KQN6^n-XN2pS?mf)xr`o{ZrF41abTj1L!pVN2AGmNVE)Fq-uWk0{I=HikHcJdvSFxKjJ$9dDnV z!xpTN>FgI5!yv0D-qKvblfHuiBppxd^cL%OuVX^l& z8NJ|9w}}=!to`1Eza7bJ21sb8W_dwBf?{seT=bli+306DCen zhj5MwJ?>&<+_Oc zF~7L$aryNh4Acn;MWG(|jFPIvZCabVmlX*vWzkig`eE(#`X@cC3R!=bZ_EaZ>|b|R z=eg24KBu$ul2&5rY;I;#adYi+o~f}HU2{e5#`bpOiXzXNfi=LB6Y;+=_i1X3ygNA3i|8*uVrp0Q4@MC-fkZ zT+z~U1yNaaIdJTNgr`S;P0()&IYX<5QU;ANZ4Df^;_DB{7GLt~&Re9Yt`&Wott(1V zebZ~zC;pD@yeWsSczV24)9P{)FBf%O+p)hi?;TwCrv)ElF)HSCxVgAwX}&y} zt}YsxuB7FaHzgE zqvuEwPmcCh>DK-)9~F&-|CDz3hpS6;)3N(ea@F^a&Qu=fl`}Lp;8K3>wg1X7_NvAE zMcW6a#aD*q;%cf#Z9}hnysyc%snL0HH$^e4a>CU2A{dYx`~wFk*|t!LylGhaBHSM* z+fU)OwEbk@&rKziWm?$^Z!V?T zv0-gLt^C26b-RoUcLzHk^xH|Sh&XQ1&50_sHs4Vn4;yn?*$P;?bm^C%@j=h$TuM(! zOuVkGja@dB{djA0Gl`QkY*74hgEZhKe2sE)#Np;ZRbDrw5H?YIpL~uWxiH<*jXWLT zeOp`JvoZ3i*pk)lPI&$GYe8TDk=(*{gl+yl|O&5(F%bLbw%F*K$11P zE@Vzj7xiB5;-O(=kpyhB=WJ|km!WL{JY{8N$>Kl-X$r=y@Wl%=h<>2&Krmw}0HQ}) z2$MhV!zVBt!b(Cx)aC*VVH7%qf)%77AeaC+{p+`P#_b4NC*_CQ$`uH)sqBo#;L(9t z)SY+k^>vrQ-OrE2on5>nB~kirhJM~UhGnbVLB#xlP+KQ8H#GUeD7dHtILg!6Hx-29hXGL}??wY91h7X?w5dpfV%Q6~QB zJK4&=FU|Q-3DLbJHU#}-Yp$pn`51FFNP?I4xKK3 z@SpGcBdJSHm!55Ewak6M;cN=x-UDnK`fE zOF=ODCw5Gx`$U{O$V|t^#zv4qzDHiVdiCDmD_`G#BAa@8VoUc{=$tndglX56gcf$h_=?gDn2_?ce#&&bz!Ur2Qq8(YZhlIAz> z-XXh#QXYMB5mHKex|=mf1RTUkGXs7v_wI~Z-^(P0`YQ>-g zDxn3H>A#!zY^B)kE{G!`sq62D<&=F7{g2acx~gDn8p78+*hTZxG+Kc_;dJ`0g3)|V zf7dcq#@|i+SKfqje6?UXp>u9>H25F9H@$}{B8bfRVydI>r=;L-qNGQQm1349C zY)#*+p?PWTo*f3qyg}`Q?nMj%q{qLE=?9#QzmJ-?>ya0casAw)hujt6KG!71JB1hH z<>66{If9x&PH}6m#f6Oo02o3L-mi*^iqg_(Yjat5$=mPlg#4p}3Z-mkjvRsUYU;Pa zyOTa|AI;M${y@>fN`5Zx#iQiIqwM2e8AgACtn?ZOg=zCcsuSdSD*d!FkCe$%Wn3hV zljzKY&=@+1nrd=8i-8Umm>^U8w^CLa;vN~fOR5keT4e6``(q5=Ya;Xy{*Uv7Cnx?= zn3P+8szyd^lNsXz(>2L#=>S2$lE}TQLUYx$DAuajqR1sdCwN!Cudh$>I=d#ti|BPx zCQ`*NK9it}R62?MG0T0AK75{Kv7?n|@;~F?VYgRzf=_A&oBOVVzqgop8?5CuHf=gI zzhz?p-9)h0Mc-Z=U#D(VOQEHlU0Ejm(0$a)N$Vp=7cgcJwXo88YLC&mBoB{&Mxr`~ z`lXC^^V|-ols{}9ixKw7$do}8`&^zw^MS*ZEi?nIp6m|Q)ANecyYY>7t5zJAcy^eS z;<#kBpZmc9C=JJtN1sFTyazQJX66Ex_e}Qmy@lL?13RskqVc8Ys;f+VPRFDO1Clnt z;%JfMI2Q|H^{1(>j(mp7)Tjj5&hn@=>wp`KoY z&@iIJ(}QyFj<6-Mu$cZa2B(~>eeQE-|6lq+vTsx9AWk5#1?-GKYCK1eI-uef2N>#e zdJ#r~cS*8{Iu-;oQYNjb7O}*JA0aA=T%n_@qvJW$RUCMWAegB(o)PeVE2Doc<7O1s z{RNwg+DKTcKua(aea?d+sJO&GL3dr(bMg_BW{4C>l)AVakNl37Ip+I!c4ot+k~H_! zPBxXrZD;fG7XB(3-v^Rbx6J5E7teE?AVD1KEOp?&s}Fj54y8nNeu}>YEi5e&(S)K- zp$C={mtD2K=7z2_xcAYVTHm?LLNRQ&z=OYqJ$rjnRC%v~x&!KjuN%l`U}gqwoqCD) zxrl+0ks{dG!g(O{gg<&T^8LFgeo7#)tyC|;k6;=@;P6EtEb}mpOylK17A0O9`4n&U z-<7My+sW3wk;*B#;@S8)+ng)mu)d&f>N5}NMBS30+w(x3FI@g332<-YvT*^KSy`A! z5QB+(@+6A$Nfd)$jp2A}vQ`#rZ7#%I=z^`|7O|ieAeF<(PRN;lXR|8TXWm9dAeWln zGzmfg+D3dlT*fjBWKL<VxF zX3#JGuKmV$lw%bWKH*bFL3eO>%a7Na6J-T{c=1%zUhx@|TI+5g@@d!D8@fmp%!@M( zKeo-=)H%w8D5~LTfOydr78?4osVVtu0o+IowU`X5s;boaZq}7qTU%c^z)W=lWIc+_ zn!P1acp7_3Ki3lGG?Wdh?U4s$fd#Yq(FSY9gZC%ph{S;d0XxkqQ2Nu~&q<`|aq01K zZB8(E*3VAdCp0U3q@|^}TD9W4aP;%qLM8&yPccv@5RV`5Tvb=cx7x#ZJzZV!YTlc! zuH*lLa=aGX+p<>>(2$FdjfhZ(bNJCCRyH=|ZeeSB7#@x$u8bnT*LppF5nTrLRE_|K z4;Sb}TNKyK&Bbon48w&pWp?@R0_BE$V;!VK- z8LyJ7hf;h$P4m#oTh%#x#Fq$$winL31bg22@pegYvH8_p?%xpXz`QouRG zp&!$?C=xi;W=|UKVNTrUEjNGq{^A}|z#46<%)i@v_rIGx7+_lY+f=5U2|XmbKP!*V zb&3Cu46)N)a{BwHWYEXv@R`gafj~;Hs21OpEx!#?!sbyFaWq&aWaV0OV?a&YOaDac zbxAcE&AIf?cYd7;{*b5qROH>KChzDxi;15OR;vPSbqjF^YtI_#onm?U^83|~?c2C1 zzxG&B9XeK)_+l&sAl-g{h(v9-RZiyW;)ng~6%ub6j#u}#7#2n9>RQ|@q)GmAs{Bd% zpq|j^ezPruZNuILDVHs4g%7p$+^oGf$)u9uv>x-tokH=!-IT>!)pdgdYO&iVpXw@{ zr_P!=HBb9Tn~|o@e3H$EeWmKnmHxBJG+rXUUr%{xGwPIA8P^XbuYCL*z29#+nDhJf zC|WF3w3)Pef`P zv5ZWoG5KOiIdKoz$lMG1d)wwg{HJ*5YnQGr6g=+|rOcsD)gw4}{A7v3kw z{NBu0D$a&s&UUx;@hcMVd$$rk6;JGvj`CES>sP*@p4yRWbxYcA@=NaHH;hHpKFnQR z7Rzg4;bts14HrA*{Iu+3%KnT+YR5h8^jvgIT=cs>v-+0txlHyo&(e~qMu9l*0EXEF zp%#USSbNV~jBcN2bUsGLWVY3sR^OXMINE*xk9{41`yFV6`h?Oo{8j=@)J&td`Dr~2 zxA_!UcaL*N`K0y;*qt1iLqMS%?E3#Q~LRfwB>*3UCU}UDR3p;^IxHo<@|DKNXZ;;O8l`Sks28>_S82$5@UTW zb05!DgqsmR?d<*tq%spgC`xN>R}7hmZ6)DrIzoJHbuL~D#;Lb7P2}$TKkTeI=JJkA zkbb^W{GhZqBP+waEjqIRBniO5c@lAwoUGGXm_9fAf{;UK)wHYVRKB!voD1c zq03}!6w;J&-mmsOI7|0S`IBJEcAKyBZFRaYB{l8#GO3t9I{QnbT}7g7` zYR&Yyx}1?{(fQ1Ys!PJTEI-@nHT`5ONklyI_pWe<|{I-oP zU{|40!+9~?+!K`7pXzy-o$aX+39e+_Iv{hwuxjv!r4P@;K639}$q9pJB0>qLy(sRu zmT5?(?^Oz?Y7MNTAa1MWKfHW?Qep8}M9`5pJ#WX%lJlN^8opC8w458_{S1MM3ZD^c|$aXv?#i(%P|FEt`+R@q61;=&`#htbv%wFqmyUaI9@k}Xu-k~tHu4TtGi_jBN>1;@g zyWrYnZ=LN;=`!VgUtTnnv~Jpda`F26V_hTMC9@xD^M-aB6uA24RgSm%UTXOLKD(gH zPn5WDts_m0WNuVZzT_{fCOW-Oam$y9EkvhSm9$Aw=F#{v`&r=!6-_VsOA35l^5qt% z+qNoO1!t#aohjRU&%@mxxJm{`_B*o){-y645bQ_--xKGQiGw|_uZ!aa)_nlL%GAb=v+{TLB z0n8+CIeQ_?mdZtO8}+S2)ZC13AfI_5(YuV9|r^8IbmKT1?vY2`aw%NL^~Qje9t zpFAMBfqFa}&2;JCrd|CtaX>O4n+COWg{NsLTf~HQdbAhWb4ClANpV#_6uPZaS)6=L zXvlY?zn;3Ma>S`n8gXejZG_bTAx6ebTzgz&FnCnJ?#U~JQsO5lkUzk-V}}?h!U?q3 z8?3Lb&X3k(SO@mZ*LK)=E=E6nY`GAUVF*M3WWse8TF+pS#*KdTNBMHxci*RVq_^KOK7F^q2j)M`_DYQR3_C^dNC2^W&JJy!o3HU5 zDOS?=(bq(i65!27(iB>m1`9H@p8jSHcs3G1Y|&?>;wvp7)=dHurc<2{ToE<#zTmjJ5pk{rT#jjs`dqKM)HEwPF6P^aRe{BcfZ9&c! z1%nzIC{cvKaS}r#qrV;erBH(4Er(MAQBm-4&F2SU_%pV#d8v75#I}Uf9bGIzJ794x zvg$wA{~DTQZEY>Gu&Cj4{1J8Tw}t-@;(2Nch_e2KarV2oCjbaoJ@VyUB$6;p%?PA} zlq)TjNLQa2A3w+%nnhO<_BnQL@Q zd|)iNth!a8=5>a&)}PWJy|C8)|L}3+Kv(V@g z1v#W$Blw^>;;R?}J04tt56m$c3KJs0!ej;*y@ydA;R&tO^wXtu zdpkRNM#f5bJqb`=7uPfJ{r(Lp3K17BtG&fygi#r^@?rWACOSG~yr2>jTqIg>g=dW( z)*Av4^71447hZ3u-T*@({5={OxFxzARr`cakn9gGdwJF?N$V79^|rOiSLxl9z2vIb z_gZpdNqEJYzx}sEM2|v`=_tAV{W&+)1v^zU_Q=-sbMabPSR~0Ib|u>0a%h$X%WIQZ zw91nBPaOppH}?$~)A4z5(Q+R=c+V7?V@F#XxhbJp3LqLuS(W5j0Au1l7y~kS!%7Mk zH`YA0<_V_V7;Lz4Gj$*b3wpC0k>$Q3YO54 zyQiimIm-j+7mYTyHUF2O{OK$O#{z-~Y=ujZU0`rPB(XsF;x$;7kO(wuzX=x|;BvuV z_L!_T9v<8Tq=gLmr4WM`!0t0b!EiJeJ!@6IL%XOw zRR3CE(xU>pmTeL>&Ngty{Td1c@M!QFhf(p)lY;McFIrfc5T0X(oLHHl1W*R(3W-74b+fZA$ z#s83^Y4yEPrvh+{^pk_rsnQs9@4wVk-82b}zhN=*8AmKLxoccb&GNI*k)vOajeQfa zGd(k-HR&&xTSK-+uaeg{&WjoT>U@!Lao5>Y4_{6(I9u4o_=VcJ(&41!*#-SZWj4D4 zSo^awgAGm=u|NDAVX;BC)N9KdTSH4*=FqMB4wnpQoPx!jIhMEP_zy3RTYcL{@T=9)uT8je>H+mHnK{Vd;I8cCL>-^zOjb zIx@mdnyT*US;-NxqHoAO{lHR3rgik}f@9|`uBqRs;$}sNkP7dADXk#17YxjX(W`k`=x&TbEeL;E%*c?F!SRr)cYaK8D;-Fh3Xgp literal 0 HcmV?d00001 From 4441a7cc1c07dc62fc04bd7428bd2f7efcdb0fd9 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:50:42 +0200 Subject: [PATCH 036/155] add screenshot --- website/docs/admin_hosts_maya.md | 5 +++-- .../maya-build_workfile_from_template.png | Bin 20676 -> 29814 bytes .../docs/assets/maya-workfile-outliner.png | Bin 0 -> 4835 bytes .../settings/template_build_workfile.png | Bin 29814 -> 12596 bytes 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 website/docs/assets/maya-workfile-outliner.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index c55dcc1b36..0ba030c26f 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -129,7 +129,7 @@ Building a workfile using a template designed by users. Helping to assert homoge Make your template. Add families and everything needed for your tasks. Here is an example template for the modeling task using a placeholder to import a gauge. -![Dirmap settings](assets/maya-workfile-outliner.png) +![maya outliner](assets/maya-workfile-outliner.png) If needed, you can add placeholders when the template needs to load some assets. **OpenPype > Template Builder > Create Placeholder** @@ -159,7 +159,8 @@ Fill in the necessary fields (the optional fields are regex filters) - **Go to Studio settings > Project > Your DCC > Templated Build Settings** - Add a profile for your task and enter path to your template -![Dirmap settings](assets/settings/template_build_workfile.png) + +![build template](assets/settings/template_build_workfile.png) **3. Build your workfile** diff --git a/website/docs/assets/maya-build_workfile_from_template.png b/website/docs/assets/maya-build_workfile_from_template.png index 336b76f8aa1ef8d22aa8b912da1cc5f4827fc5e8..7ef87861fe97322a3b23c28ee3644b76ce995e9d 100644 GIT binary patch literal 29814 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 literal 20676 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 Z{|`+)aTxx~sMG)e002ovPDHLkV1iGtk!t_| diff --git a/website/docs/assets/maya-workfile-outliner.png b/website/docs/assets/maya-workfile-outliner.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd1bbd03bce52041ad511dac138479494641361 GIT binary patch literal 4835 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 X`o37wi;`y6#S|Ob%*wRV#3TNnn_AI~ literal 0 HcmV?d00001 diff --git a/website/docs/assets/settings/template_build_workfile.png b/website/docs/assets/settings/template_build_workfile.png index 7ef87861fe97322a3b23c28ee3644b76ce995e9d..1bea5b01f5f3145e2d1aa5d46545c3739aaf2584 100644 GIT binary patch literal 12596 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 literal 29814 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 From 90754fb0b8d36eb0b34cba6da48f672505470f0e Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:51:02 +0200 Subject: [PATCH 037/155] modify readme doc --- website/docs/admin_hosts_maya.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 0ba030c26f..0e77f29fc2 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -160,7 +160,7 @@ Fill in the necessary fields (the optional fields are regex filters) - **Go to Studio settings > Project > Your DCC > Templated Build Settings** - Add a profile for your task and enter path to your template -![build template](assets/settings/template_build_workfile.png) +![setting build template](assets/settings/template_build_workfile.png) **3. Build your workfile** @@ -168,6 +168,6 @@ Fill in the necessary fields (the optional fields are regex filters) - Build your workfile -![Dirmap settings](assets/maya-build_workfile_from_template.png) +![maya build template](assets/maya-build_workfile_from_template.png) From d3c2dc57d8f39865c3f3900db1be1f911b6436c9 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 15:02:43 +0200 Subject: [PATCH 038/155] fix linter --- openpype/hosts/maya/api/lib_template_builder.py | 6 +++++- openpype/lib/build_template_exceptions.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bed4291e3d..e5254b7f87 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -43,7 +43,11 @@ def create_placeholder(): placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) - placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') + placeholder_full_name = cmds.ls( + selection[0], + long=True)[0] + '|' + placeholder.replace('|', + '' + ) if selection: cmds.parent(placeholder, selection[0]) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py index d781eff204..7a5075e3dc 100644 --- a/openpype/lib/build_template_exceptions.py +++ b/openpype/lib/build_template_exceptions.py @@ -32,4 +32,4 @@ class TemplateAlreadyImported(Exception): class TemplateLoadingFailed(Exception): """Error raised whend Template loader was unable to load the template""" - pass \ No newline at end of file + pass From bc9c5b183171b9ef03b88062c7536a5effed3ae1 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 15:22:33 +0200 Subject: [PATCH 039/155] fix linter lengh line --- openpype/hosts/maya/api/lib_template_builder.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index e5254b7f87..a30b3868b0 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -43,11 +43,8 @@ def create_placeholder(): placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) - placeholder_full_name = cmds.ls( - selection[0], - long=True)[0] + '|' + placeholder.replace('|', - '' - ) + placeholder_full_name = cmds.ls(selection[0], long=True)[ + 0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) From ba1abf8b15e1476b430180bc63b1a3801ff118ef Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 14 Jun 2022 10:35:41 +0200 Subject: [PATCH 040/155] add task name field in build templated workfile settings --- openpype/settings/defaults/project_settings/maya.json | 1 + .../schemas/schema_templated_workfile_build.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2e0e30b74b..453706ff88 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -722,6 +722,7 @@ "profiles": [ { "task_types": [], + "tasks": [], "path": "/path/to/your/template" } ] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json index 01e74f64b0..a591facf98 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -16,6 +16,12 @@ "label": "Task types", "type": "task-types-enum" }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, { "key": "path", "label": "Path to template", From ef7627199eb8297196c9d0a778e222132d742be2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 14 Jun 2022 11:37:08 +0200 Subject: [PATCH 041/155] add a task name verification for template loader --- openpype/lib/abstract_template_loader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 159d5c8f6c..e296e3207f 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -160,6 +160,8 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue + if prf['tasks'] and task_name not in prf['tasks']: + continue path = prf['path'] break else: # IF no template were found (no break happened) From 01f2c59049be47fce42a5bfdcf67ef1227be1d11 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 18 Jul 2022 15:15:40 +0200 Subject: [PATCH 042/155] the update placeholder keep placeholder info when canceled or closed --- openpype/hosts/maya/api/lib_template_builder.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index a30b3868b0..855c72e361 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -107,11 +107,13 @@ def update_placeholder(): placeholder = placeholder[0] args = placeholder_window(get_placeholder_attributes(placeholder)) - # delete placeholder attributes - delete_placeholder_attributes(placeholder) + if not args: return # operation canceled + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + options = create_options(args) imprint(placeholder, options) From e6cad709cd5904087f687a4e2ec5a15641ab48e0 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 18 Jul 2022 18:43:34 +0200 Subject: [PATCH 043/155] fix error when updating workfile from template with empty scene --- openpype/hosts/maya/api/template_loader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index 0e346ca411..c7946b6ad3 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -80,7 +80,11 @@ class MayaTemplateLoader(AbstractTemplateLoader): return [attribute.rpartition('.')[0] for attribute in attributes] def get_loaded_containers_by_id(self): - containers = cmds.sets('AVALON_CONTAINERS', q=True) + try: + containers = cmds.sets("AVALON_CONTAINERS", q=True) + except ValueError: + return None + return [ cmds.getAttr(container + '.representation') for container in containers] From b5fb016331e3a86397f6337f44e4c885caf9cff1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:10:08 +0200 Subject: [PATCH 044/155] moved abstract template loader into openpype/pipeline/workfile --- openpype/{lib => pipeline/workfile}/abstract_template_loader.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/{lib => pipeline/workfile}/abstract_template_loader.py (100%) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py similarity index 100% rename from openpype/lib/abstract_template_loader.py rename to openpype/pipeline/workfile/abstract_template_loader.py From b1f2831868001431ab5b949cf2a85729a9adfb04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:12:04 +0200 Subject: [PATCH 045/155] moved 'get_loaders_by_name' to load utils --- openpype/lib/avalon_context.py | 15 --------------- openpype/pipeline/load/__init__.py | 2 ++ openpype/pipeline/load/utils.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 316c8ad67e..86902cac56 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -943,21 +943,6 @@ def collect_last_version_repres(asset_entities): return output -@with_pipeline_io -def get_loaders_by_name(): - from openpype.pipeline import discover_loader_plugins - - loaders_by_name = {} - for loader in discover_loader_plugins(): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError( - "Duplicated loader name {} !".format(loader_name) - ) - loaders_by_name[loader_name] = loader - return loaders_by_name - - class BuildWorkfile: """Wrapper for build workfile process. diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index e46d9f152b..b6bdd13d50 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -16,6 +16,7 @@ from .utils import ( switch_container, get_loader_identifier, + get_loaders_by_name, get_representation_path_from_context, get_representation_path, @@ -61,6 +62,7 @@ __all__ = ( "switch_container", "get_loader_identifier", + "get_loaders_by_name", "get_representation_path_from_context", "get_representation_path", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index fe5102353d..9945e1fce4 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -369,6 +369,20 @@ def get_loader_identifier(loader): return loader.__name__ +def get_loaders_by_name(): + from .plugins import discover_loader_plugins + + loaders_by_name = {} + for loader in discover_loader_plugins(): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + def _get_container_loader(container): """Return the Loader corresponding to the container""" from .plugins import discover_loader_plugins From b2b6ffe0e4290840fc1ca1b5c98174f2bdfcbfaf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:13:56 +0200 Subject: [PATCH 046/155] updated 'collect_last_version_repres' with latest develop --- openpype/lib/avalon_context.py | 68 +++++++++++++++------------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 86902cac56..4b552d13ed 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -847,7 +847,7 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): @with_pipeline_io -def collect_last_version_repres(asset_entities): +def collect_last_version_repres(asset_docs): """Collect subsets, versions and representations for asset_entities. Args: @@ -880,64 +880,56 @@ def collect_last_version_repres(asset_entities): ``` """ - if not asset_entities: - return {} + output = {} + if not asset_docs: + return output - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + asset_docs_by_ids = {asset["_id"]: asset for asset in asset_docs} - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) + project_name = legacy_io.active_project() + subsets = list(get_subsets( + project_name, asset_ids=asset_docs_by_ids.keys() + )) subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) + last_version_by_subset_id = get_last_versions( + project_name, subset_entity_by_ids.keys() + ) + last_version_docs_by_id = { + version["_id"]: version + for version in last_version_by_subset_id.values() + } + repre_docs = get_representations( + project_name, version_ids=last_version_docs_by_id.keys() + ) - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_doc = last_version_docs_by_id[version_id] - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) + subset_id = version_doc["parent"] + subset_doc = subset_entity_by_ids[subset_id] - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] + asset_id = subset_doc["parent"] + asset_doc = asset_docs_by_ids[asset_id] if asset_id not in output: output[asset_id] = { - "asset_entity": asset, + "asset_entity": asset_doc, "subsets": {} } if subset_id not in output[asset_id]["subsets"]: output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, + "subset_entity": subset_doc, "version": { - "version_entity": version, + "version_entity": version_doc, "repres": [] } } output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre + repre_doc ) return output From 9b4b44ef3bdf490fca2a4df0f3451143a09e555c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:17:26 +0200 Subject: [PATCH 047/155] moved build template code into workfile --- openpype/{lib => pipeline/workfile}/build_template.py | 0 openpype/{lib => pipeline/workfile}/build_template_exceptions.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename openpype/{lib => pipeline/workfile}/build_template.py (100%) rename openpype/{lib => pipeline/workfile}/build_template_exceptions.py (100%) diff --git a/openpype/lib/build_template.py b/openpype/pipeline/workfile/build_template.py similarity index 100% rename from openpype/lib/build_template.py rename to openpype/pipeline/workfile/build_template.py diff --git a/openpype/lib/build_template_exceptions.py b/openpype/pipeline/workfile/build_template_exceptions.py similarity index 100% rename from openpype/lib/build_template_exceptions.py rename to openpype/pipeline/workfile/build_template_exceptions.py From 6462bf15d04ad53eaed484069e70f2c2312f0a2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:24:16 +0200 Subject: [PATCH 048/155] fixed imports --- .../workfile/abstract_template_loader.py | 24 ++++++++++--------- openpype/pipeline/workfile/build_template.py | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index e296e3207f..e95b89b518 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -4,23 +4,25 @@ from abc import ABCMeta, abstractmethod import traceback import six - -from openpype.settings import get_project_settings -from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name -from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io, load - +import logging from functools import reduce -from openpype.lib.build_template_exceptions import ( +from openpype.settings import get_project_settings +from openpype.lib import get_linked_assets, PypeLogger as Logger +from openpype.pipeline import legacy_io, Anatomy +from openpype.pipeline.load import ( + get_loaders_by_name, + get_representation_context, + load_with_repre_context, +) + +from .build_template_exceptions import ( TemplateAlreadyImported, TemplateLoadingFailed, TemplateProfileNotFound, TemplateNotFound ) -import logging - log = logging.getLogger(__name__) @@ -289,8 +291,8 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - repre = load.get_representation_context(last_representation) - return load.load_with_repre_context( + repre = get_representation_context(last_representation) + return load_with_repre_context( loaders_by_name[placeholder.loader], repre, options=parse_loader_args(placeholder.data['loader_args'])) diff --git a/openpype/pipeline/workfile/build_template.py b/openpype/pipeline/workfile/build_template.py index 7f749cbec2..f4b57218fb 100644 --- a/openpype/pipeline/workfile/build_template.py +++ b/openpype/pipeline/workfile/build_template.py @@ -1,6 +1,6 @@ -from openpype.pipeline import registered_host -from openpype.lib import classes_from_module from importlib import import_module +from openpype.lib import classes_from_module +from openpype.pipeline import registered_host from .abstract_template_loader import ( AbstractPlaceholder, From 5dfb12a217f24e5551ec3f4a982823254efdb00e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:24:44 +0200 Subject: [PATCH 049/155] logger is created dynamically on demand and is using class name --- openpype/pipeline/workfile/abstract_template_loader.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index e95b89b518..27823479cf 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -72,6 +72,7 @@ class AbstractTemplateLoader: """ def __init__(self, placeholder_class): + self._log = None self.loaders_by_name = get_loaders_by_name() self.current_asset = legacy_io.Session["AVALON_ASSET"] @@ -91,8 +92,6 @@ class AbstractTemplateLoader: .get("type") ) - self.log = Logger().get_logger("BUILD TEMPLATE") - self.log.info( "BUILDING ASSET FROM TEMPLATE :\n" "Starting templated build for {asset} in {project}\n\n" @@ -112,6 +111,12 @@ class AbstractTemplateLoader: "There is no registered loaders. No assets will be loaded") return + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + def template_already_imported(self, err_msg): """In case template was already loaded. Raise the error as a default action. From 764207d033fc049f6726f901a99732c928595768 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:25:04 +0200 Subject: [PATCH 050/155] fix missing import 'get_loaders_by_name' --- openpype/lib/avalon_context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 4b552d13ed..e60dbb9e8f 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -992,6 +992,9 @@ class BuildWorkfile: ... }] """ + + from openpype.pipeline.load import get_loaders_by_name + # Get current asset name and entity project_name = legacy_io.active_project() current_asset_name = legacy_io.Session["AVALON_ASSET"] From fe38df50bff954993570cd113371044dde4a5e43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:26:18 +0200 Subject: [PATCH 051/155] removed 'get_loaders_by_name' from openpype lib init file --- openpype/lib/__init__.py | 2 -- openpype/pipeline/workfile/__init__.py | 0 2 files changed, 2 deletions(-) create mode 100644 openpype/pipeline/workfile/__init__.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index f4efffd726..fb52a9aca7 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -135,7 +135,6 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, - get_loaders_by_name, BuildWorkfile, @@ -307,7 +306,6 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", - "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/pipeline/workfile/__init__.py b/openpype/pipeline/workfile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From c9ac330e2ebedc6e9900e0d2e6207a20326d0139 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:31:18 +0200 Subject: [PATCH 052/155] fixed imports in maya --- openpype/hosts/maya/api/menu.py | 6 +++--- openpype/hosts/maya/api/template_loader.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c0bad7092f..833fbae881 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -8,12 +8,12 @@ import maya.cmds as cmds from openpype.api import BuildWorkfile -from openpype.lib.build_template import ( +from openpype.settings import get_project_settings +from openpype.pipeline import legacy_io +from openpype.pipeline.workfile.build_template import ( build_workfile_template, update_workfile_template ) -from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index c7946b6ad3..6b225442e7 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -1,11 +1,13 @@ from maya import cmds from openpype.pipeline import legacy_io -from openpype.lib.abstract_template_loader import ( +from openpype.pipeline.workfile.abstract_template_loader import ( AbstractPlaceholder, AbstractTemplateLoader ) -from openpype.lib.build_template_exceptions import TemplateAlreadyImported +from openpype.pipeline.workfile.build_template_exceptions import ( + TemplateAlreadyImported +) PLACEHOLDER_SET = 'PLACEHOLDERS_SET' From 1e8cf2a6ea87ded1131d5d3012cdd5980dc2f183 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:36:04 +0200 Subject: [PATCH 053/155] make sure '_log' attribute is available before abc init --- openpype/pipeline/workfile/abstract_template_loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 27823479cf..3d942a0bdd 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -71,9 +71,9 @@ class AbstractTemplateLoader: as placeholders. Depending on current host """ - def __init__(self, placeholder_class): - self._log = None + _log = None + def __init__(self, placeholder_class): self.loaders_by_name = get_loaders_by_name() self.current_asset = legacy_io.Session["AVALON_ASSET"] self.project_name = legacy_io.Session["AVALON_PROJECT"] From 6bb28d16df22e4d5c4cf6e763a85a545ba6da833 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Jul 2022 11:53:51 +0200 Subject: [PATCH 054/155] fix build template and added few comments --- .../workfile/abstract_template_loader.py | 30 +++++++++++++------ openpype/pipeline/workfile/build_template.py | 9 +++++- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 3d942a0bdd..00bc8f15a7 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -7,6 +7,7 @@ import six import logging from functools import reduce +from openpype.client import get_asset_by_name from openpype.settings import get_project_settings from openpype.lib import get_linked_assets, PypeLogger as Logger from openpype.pipeline import legacy_io, Anatomy @@ -74,18 +75,25 @@ class AbstractTemplateLoader: _log = None def __init__(self, placeholder_class): + # TODO template loader should expect host as and argument + # - host have all responsibility for most of code (also provide + # placeholder class) + # - also have responsibility for current context + # - this won't work in DCCs where multiple workfiles with + # different contexts can be opened at single time + # - template loader should have ability to change context + project_name = legacy_io.active_project() + asset_name = legacy_io.Session["AVALON_ASSET"] + self.loaders_by_name = get_loaders_by_name() - self.current_asset = legacy_io.Session["AVALON_ASSET"] - self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.current_asset = asset_name + self.project_name = project_name self.host_name = legacy_io.Session["AVALON_APP"] self.task_name = legacy_io.Session["AVALON_TASK"] self.placeholder_class = placeholder_class - self.current_asset_docs = legacy_io.find_one({ - "type": "asset", - "name": self.current_asset - }) + self.current_asset_doc = get_asset_by_name(project_name, asset_name) self.task_type = ( - self.current_asset_docs + self.current_asset_doc .get("data", {}) .get("tasks", {}) .get(self.task_name, {}) @@ -218,7 +226,7 @@ class AbstractTemplateLoader: loaders_by_name = self.loaders_by_name current_asset = self.current_asset linked_assets = [asset['name'] for asset - in get_linked_assets(self.current_asset_docs)] + in get_linked_assets(self.current_asset_doc)] ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() @@ -270,7 +278,11 @@ class AbstractTemplateLoader: self.postload(placeholder) def get_placeholder_representations( - self, placeholder, current_asset, linked_assets): + self, placeholder, current_asset, linked_assets + ): + # TODO This approach must be changed. Placeholders should return + # already prepared data and not query them here. + # - this is impossible to handle using query functions placeholder_db_filters = placeholder.convert_to_db_filters( current_asset, linked_assets) diff --git a/openpype/pipeline/workfile/build_template.py b/openpype/pipeline/workfile/build_template.py index f4b57218fb..df6fe3514a 100644 --- a/openpype/pipeline/workfile/build_template.py +++ b/openpype/pipeline/workfile/build_template.py @@ -1,5 +1,6 @@ from importlib import import_module from openpype.lib import classes_from_module +from openpype.host import HostBase from openpype.pipeline import registered_host from .abstract_template_loader import ( @@ -35,7 +36,13 @@ def update_workfile_template(args): def build_template_loader(): - host_name = registered_host().__name__.partition('.')[2] + # TODO refactor to use advantage of 'HostBase' and don't import dynamically + # - hosts should have methods that gives option to return builders + host = registered_host() + if isinstance(host, HostBase): + host_name = host.name + else: + host_name = host.__name__.partition('.')[2] module_path = _module_path_format.format(host=host_name) module = import_module(module_path) if not module: From 2d601d051a9b59509c6af159c06f8424591af444 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 16:42:54 +0200 Subject: [PATCH 055/155] give ability to query by representation context and regex --- openpype/client/entities.py | 108 +++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 14 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index dd5d831ecf..57c38784b0 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -7,6 +7,7 @@ that has project name as a context (e.g. on 'ProjectEntity'?). """ import os +import re import collections import six @@ -1035,17 +1036,70 @@ def get_representation_by_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, re.Pattern): + regexes.append(value) + elif isinstance(value, (list, tuple, set)): + for item in value: + if isinstance(item, re.Pattern): + 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, - extensions, + context_filters, names_by_version_ids, standard, archived, fields ): + default_output = [] repre_types = [] if standard: repre_types.append("representation") @@ -1053,7 +1107,7 @@ def _get_representations( repre_types.append("archived_representation") if not repre_types: - return [] + return default_output if len(repre_types) == 1: query_filter = {"type": repre_types[0]} @@ -1063,25 +1117,21 @@ def _get_representations( if representation_ids is not None: representation_ids = _convert_ids(representation_ids) if not representation_ids: - return [] + return default_output query_filter["_id"] = {"$in": representation_ids} if representation_names is not None: if not representation_names: - return [] + 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 [] + return default_output query_filter["parent"] = {"$in": version_ids} - if extensions is not None: - if not extensions: - return [] - query_filter["context.ext"] = {"$in": list(extensions)} - + or_queries = [] if names_by_version_ids is not None: or_query = [] for version_id, names in names_by_version_ids.items(): @@ -1091,8 +1141,35 @@ def _get_representations( "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 [] - query_filter["$or"] = or_query + _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": + or_queries.append(value) + else: + query_filter[key] = 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) @@ -1104,7 +1181,7 @@ def get_representations( representation_ids=None, representation_names=None, version_ids=None, - extensions=None, + context_filters=None, names_by_version_ids=None, archived=False, standard=True, @@ -1122,8 +1199,8 @@ def get_representations( as filter. Filter ignored if 'None' is passed. version_ids (Iterable[str]): Subset ids used as parent filter. Filter ignored if 'None' is passed. - extensions (Iterable[str]): Filter by extension of main representation - file (without dot). + context_filters (Dict[str, List[str, re.Pattern]]): 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. @@ -1140,6 +1217,7 @@ def get_representations( representation_names=representation_names, version_ids=version_ids, extensions=extensions, + context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=True, archived=archived, @@ -1153,6 +1231,7 @@ def get_archived_representations( representation_names=None, version_ids=None, extensions=None, + context_filters=None, names_by_version_ids=None, fields=None ): @@ -1185,6 +1264,7 @@ def get_archived_representations( representation_names=representation_names, version_ids=version_ids, extensions=extensions, + context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=False, archived=True, From c65dd9747f5197868a9153fc109915ed654122ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:15:13 +0200 Subject: [PATCH 056/155] added new method 'get_representations' to get representations from placeholder --- .../workfile/abstract_template_loader.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 00bc8f15a7..0a422f5cca 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -456,8 +456,25 @@ class AbstractPlaceholder: @abstractmethod def clean(self): - """Clean placeholder from hierarchy after loading assets. + """Clean placeholder from hierarchy after loading assets.""" + + pass + + @abstractmethod + def get_representations(self, current_asset, linked_assets): + """Query representations based on placeholder data. + + Args: + current_asset (str): Name of current + context asset. + linked_assets (List[str]): Names of assets + linked to current context asset. + + Returns: + Iterable[Dict[str, Any]]: Representations that are matching + placeholder filters. """ + pass @abstractmethod From da8e25f4a1b7ea89bf9c7cac62c8a3ea10fbb9e6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:16:23 +0200 Subject: [PATCH 057/155] use 'get_representations' instead of 'convert_to_db_filters' --- .../workfile/abstract_template_loader.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 0a422f5cca..a2505c061e 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -280,19 +280,16 @@ class AbstractTemplateLoader: def get_placeholder_representations( self, placeholder, current_asset, linked_assets ): - # TODO This approach must be changed. Placeholders should return - # already prepared data and not query them here. - # - this is impossible to handle using query functions - placeholder_db_filters = placeholder.convert_to_db_filters( + placeholder_representations = placeholder.get_representations( current_asset, - linked_assets) - # get representation by assets - for db_filter in placeholder_db_filters: - placeholder_representations = list(legacy_io.find(db_filter)) - for representation in reduce(update_representations, - placeholder_representations, - dict()).values(): - yield representation + linked_assets + ) + for repre_doc in reduce( + update_representations, + placeholder_representations, + dict() + ).values(): + yield repre_doc def load_data_is_incorrect( self, placeholder, last_representation, ignored_ids): From 0e0cec5e0146a3001a4a349360324346fd0ab961 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:19:47 +0200 Subject: [PATCH 058/155] pass asset documents instead of just names --- .../workfile/abstract_template_loader.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index a2505c061e..96012eba36 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -223,10 +223,10 @@ class AbstractTemplateLoader: Returns: None """ + loaders_by_name = self.loaders_by_name - current_asset = self.current_asset - linked_assets = [asset['name'] for asset - in get_linked_assets(self.current_asset_doc)] + current_asset_doc = self.current_asset_doc + linked_assets = get_linked_assets(current_asset_doc) ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() @@ -239,7 +239,7 @@ class AbstractTemplateLoader: )) placeholder_representations = self.get_placeholder_representations( placeholder, - current_asset, + current_asset_doc, linked_assets ) @@ -278,11 +278,11 @@ class AbstractTemplateLoader: self.postload(placeholder) def get_placeholder_representations( - self, placeholder, current_asset, linked_assets + self, placeholder, current_asset_doc, linked_asset_docs ): placeholder_representations = placeholder.get_representations( - current_asset, - linked_assets + current_asset_doc, + linked_asset_docs ) for repre_doc in reduce( update_representations, @@ -458,13 +458,13 @@ class AbstractPlaceholder: pass @abstractmethod - def get_representations(self, current_asset, linked_assets): + def get_representations(self, current_asset_doc, linked_asset_docs): """Query representations based on placeholder data. Args: - current_asset (str): Name of current + current_asset_doc (Dict[str, Any]): Document of current context asset. - linked_assets (List[str]): Names of assets + linked_asset_docs (List[Dict[str, Any]]): Documents of assets linked to current context asset. Returns: From ef674857f85f360954b4d6e2c6f6c0c4acf3f711 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:29:34 +0200 Subject: [PATCH 059/155] implemented get_representations for maya placeholder --- openpype/hosts/maya/api/template_loader.py | 80 +++++++++++----------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index 6b225442e7..f553730186 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -1,5 +1,7 @@ +import re from maya import cmds +from openpype.client import get_representations from openpype.pipeline import legacy_io from openpype.pipeline.workfile.abstract_template_loader import ( AbstractPlaceholder, @@ -191,48 +193,48 @@ class MayaPlaceholder(AbstractPlaceholder): cmds.hide(node) cmds.setAttr(node + '.hiddenInOutliner', True) - def convert_to_db_filters(self, current_asset, linked_asset): - if self.data['builder_type'] == "context_asset": - return [ - { - "type": "representation", - "context.asset": { - "$eq": current_asset, - "$regex": self.data['asset'] - }, - "context.subset": {"$regex": self.data['subset']}, - "context.hierarchy": {"$regex": self.data['hierarchy']}, - "context.representation": self.data['representation'], - "context.family": self.data['family'], - } - ] + def get_representations(self, current_asset_doc, linked_asset_docs): + project_name = legacy_io.active_project() - elif self.data['builder_type'] == "linked_asset": - return [ - { - "type": "representation", - "context.asset": { - "$eq": asset_name, - "$regex": self.data['asset'] - }, - "context.subset": {"$regex": self.data['subset']}, - "context.hierarchy": {"$regex": self.data['hierarchy']}, - "context.representation": self.data['representation'], - "context.family": self.data['family'], - } for asset_name in linked_asset - ] + builder_type = self.data["builder_type"] + if builder_type == "context_asset": + context_filters = { + "asset": [current_asset_doc["name"]], + "subset": [re.compile(self.data["subset"])], + "hierarchy": [re.compile(self.data["hierarchy"])], + "representations": [self.data["representation"]], + "family": [self.data["family"]] + } + + elif builder_type != "linked_asset": + context_filters = { + "asset": [re.compile(self.data["asset"])], + "subset": [re.compile(self.data["subset"])], + "hierarchy": [re.compile(self.data["hierarchy"])], + "representation": [self.data["representation"]], + "family": [self.data["family"]] + } else: - return [ - { - "type": "representation", - "context.asset": {"$regex": self.data['asset']}, - "context.subset": {"$regex": self.data['subset']}, - "context.hierarchy": {"$regex": self.data['hierarchy']}, - "context.representation": self.data['representation'], - "context.family": self.data['family'], - } - ] + asset_regex = re.compile(self.data["asset"]) + linked_asset_names = [] + for asset_doc in linked_asset_docs: + asset_name = asset_doc["name"] + if asset_regex.match(asset_name): + linked_asset_names.append(asset_name) + + context_filters = { + "asset": linked_asset_names, + "subset": [re.compile(self.data["subset"])], + "hierarchy": [re.compile(self.data["hierarchy"])], + "representation": [self.data["representation"]], + "family": [self.data["family"]], + } + + return list(get_representations( + project_name, + context_filters=context_filters + )) def err_message(self): return ( From a6406f72d36d8eb748404af8fc6e6d61c6c6b451 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:39:57 +0200 Subject: [PATCH 060/155] added logger to placeholder --- .../pipeline/workfile/abstract_template_loader.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 96012eba36..d934c50daf 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -397,8 +397,19 @@ class AbstractPlaceholder: optional_attributes = {} def __init__(self, node): + self._log = None self.get_data(node) + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(repr(self)) + return self._log + + def __repr__(self): + return "< {} {} >".format(self.__class__.__name__, self.name) + + def order(self): """Get placeholder order. Order is used to sort them by priority @@ -436,9 +447,9 @@ class AbstractPlaceholder: Bool: True if every attributes are a key of data """ if set(self.attributes).issubset(self.data.keys()): - print("Valid placeholder : {}".format(self.data["node"])) + self.log.debug("Valid placeholder: {}".format(self.data["node"])) return True - print("Placeholder is not valid : {}".format(self.data["node"])) + self.log.info("Placeholder is not valid: {}".format(self.data["node"])) return False @abstractmethod From 8b7531b97775d3facefde41682dd19e9dd3e11f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:44:03 +0200 Subject: [PATCH 061/155] added helper attributes to placeholder so there is no need to access it's 'data' --- .../workfile/abstract_template_loader.py | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index d934c50daf..5ecc154ea4 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -231,11 +231,11 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() self.log.debug("Placeholders found in template: {}".format( - [placeholder.data['node'] for placeholder in placeholders] + [placeholder.name] for placeholder in placeholders] )) for placeholder in placeholders: self.log.debug("Start to processing placeholder {}".format( - placeholder.data['node'] + placeholder.name )) placeholder_representations = self.get_placeholder_representations( placeholder, @@ -246,7 +246,7 @@ class AbstractTemplateLoader: if not placeholder_representations: self.log.info( "There's no representation for this placeholder: " - "{}".format(placeholder.data['node']) + "{}".format(placeholder.name) ) continue @@ -264,8 +264,8 @@ class AbstractTemplateLoader: "Loader arguments used : {}".format( representation['context']['asset'], representation['context']['subset'], - placeholder.loader, - placeholder.data['loader_args'])) + placeholder.loader_name, + placeholder.loader_args)) try: container = self.load( @@ -307,19 +307,22 @@ class AbstractTemplateLoader: def load(self, placeholder, loaders_by_name, last_representation): repre = get_representation_context(last_representation) return load_with_repre_context( - loaders_by_name[placeholder.loader], + loaders_by_name[placeholder.loader_name], repre, - options=parse_loader_args(placeholder.data['loader_args'])) + options=parse_loader_args(placeholder.loader_args)) def load_succeed(self, placeholder, container): placeholder.parent_in_hierarchy(container) def load_failed(self, placeholder, last_representation): - self.log.warning("Got error trying to load {}:{} with {}\n\n" - "{}".format(last_representation['context']['asset'], - last_representation['context']['subset'], - placeholder.loader, - traceback.format_exc())) + self.log.warning( + "Got error trying to load {}:{} with {}".format( + last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader_name + ), + exc_info=True + ) def postload(self, placeholder): placeholder.clean() @@ -398,6 +401,7 @@ class AbstractPlaceholder: def __init__(self, node): self._log = None + self._name = node self.get_data(node) @property @@ -409,6 +413,17 @@ class AbstractPlaceholder: def __repr__(self): return "< {} {} >".format(self.__class__.__name__, self.name) + @property + def name(self): + return self._name + + @property + def loader_args(self): + return self.data["loader_args"] + + @property + def builder_type(self): + return self.data["builder_type"] def order(self): """Get placeholder order. @@ -423,12 +438,15 @@ class AbstractPlaceholder: return self.data.get('order') @property - def loader(self): - """Return placeholder loader type + def loader_name(self): + """Return placeholder loader type. + Returns: - string: Loader name + str: Loader name that will be used to load placeholder + representations. """ - return self.data.get('loader') + + return self.data["loader"] @property def is_context(self): From 2d7910a26410936f1d23282b9011780cccfc8680 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:46:14 +0200 Subject: [PATCH 062/155] renamed 'attributes' to 'required_keys' and 'optional_attributes' to 'optional_keys' --- openpype/hosts/maya/api/template_loader.py | 8 +++-- .../workfile/abstract_template_loader.py | 35 ++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index f553730186..ecffafc93d 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -98,11 +98,11 @@ class MayaPlaceholder(AbstractPlaceholder): """Concrete implementation of AbstractPlaceholder for maya """ - optional_attributes = {'asset', 'subset', 'hierarchy'} + optional_keys = {'asset', 'subset', 'hierarchy'} def get_data(self, node): user_data = dict() - for attr in self.attributes.union(self.optional_attributes): + for attr in self.required_keys.union(self.optional_keys): attribute_name = '{}.{}'.format(node, attr) if not cmds.attributeQuery(attr, node=node, exists=True): print("{} not found".format(attribute_name)) @@ -112,7 +112,9 @@ class MayaPlaceholder(AbstractPlaceholder): asString=True) user_data['parent'] = ( cmds.getAttr(node + '.parent', asString=True) - or node.rpartition('|')[0] or "") + or node.rpartition('|')[0] + or "" + ) user_data['node'] = node if user_data['parent']: siblings = cmds.listRelatives(user_data['parent'], children=True) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 5ecc154ea4..56fb31fa0c 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -377,15 +377,17 @@ class AbstractTemplateLoader: @six.add_metaclass(ABCMeta) class AbstractPlaceholder: - """Abstraction of placeholders logic + """Abstraction of placeholders logic. + Properties: - attributes: A list of mandatory attribute to decribe placeholder + required_keys: A list of mandatory keys to decribe placeholder and assets to load. - optional_attributes: A list of optional attribute to decribe + optional_keys: A list of optional keys to decribe placeholder and assets to load loader: Name of linked loader to use while loading assets is_context: Is placeholder linked to context asset (or to linked assets) + Methods: is_repres_valid: loader: @@ -395,9 +397,15 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'family', 'representation', - 'order', 'loader', 'loader_args'} - optional_attributes = {} + required_keys = { + "builder_type", + "family", + "representation", + "order", + "loader", + "loader_args" + } + optional_keys = {} def __init__(self, node): self._log = None @@ -459,15 +467,18 @@ class AbstractPlaceholder: return self.data.get('builder_type') == 'context_asset' def is_valid(self): - """Test validity of placeholder - i.e.: every attributes exists in placeholder data + """Test validity of placeholder. + + i.e.: every required key exists in placeholder data + Returns: - Bool: True if every attributes are a key of data + bool: True if every key is in data """ - if set(self.attributes).issubset(self.data.keys()): - self.log.debug("Valid placeholder: {}".format(self.data["node"])) + + if set(self.required_keys).issubset(self.data.keys()): + self.log.debug("Valid placeholder : {}".format(self.name)) return True - self.log.info("Placeholder is not valid: {}".format(self.data["node"])) + self.log.info("Placeholder is not valid : {}".format(self.name)) return False @abstractmethod From 736123d1c2496df1604d1b0c84df5a2646cc51f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:48:47 +0200 Subject: [PATCH 063/155] modified 'is_context' property --- .../pipeline/workfile/abstract_template_loader.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 56fb31fa0c..a1d188ea6c 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -458,14 +458,22 @@ class AbstractPlaceholder: @property def is_context(self): - """Return placeholder type + """Check if is placeholder context type. + context_asset: For loading current asset linked_asset: For loading linked assets + + Question: + There seems to be more build options and this property is not used, + should be removed? + Returns: bool: true if placeholder is a context placeholder """ - return self.data.get('builder_type') == 'context_asset' + return self.builder_type == "context_asset" + + @property def is_valid(self): """Test validity of placeholder. From 49799c2d8871a52fb1fd8210b31a1e51fd5f3f2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Aug 2022 18:26:38 +0200 Subject: [PATCH 064/155] fix merge conflict --- openpype/pipeline/workfile/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index a1d188ea6c..5d8d79397a 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -231,7 +231,7 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() self.log.debug("Placeholders found in template: {}".format( - [placeholder.name] for placeholder in placeholders] + [placeholder.name for placeholder in placeholders] )) for placeholder in placeholders: self.log.debug("Start to processing placeholder {}".format( From 7d1f1bb064190873beee61c0a4eb4df598747c88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Aug 2022 09:50:11 +0200 Subject: [PATCH 065/155] remove extensions arguments --- openpype/client/entities.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 57c38784b0..a3fcd01f80 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1216,7 +1216,6 @@ def get_representations( representation_ids=representation_ids, representation_names=representation_names, version_ids=version_ids, - extensions=extensions, context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=True, @@ -1230,7 +1229,6 @@ def get_archived_representations( representation_ids=None, representation_names=None, version_ids=None, - extensions=None, context_filters=None, names_by_version_ids=None, fields=None @@ -1247,8 +1245,6 @@ def get_archived_representations( as filter. Filter ignored if 'None' is passed. version_ids (Iterable[str]): Subset ids used as parent filter. Filter ignored if 'None' is passed. - extensions (Iterable[str]): Filter by extension of main representation - file (without dot). names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering using version ids and list of names under the version. fields (Iterable[str]): Fields that should be returned. All fields are @@ -1263,7 +1259,6 @@ def get_archived_representations( representation_ids=representation_ids, representation_names=representation_names, version_ids=version_ids, - extensions=extensions, context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=False, From 8db8ada9642bcdf2c5f364fbf78c902344b1613e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:46:32 +0200 Subject: [PATCH 066/155] changed 'node' variable to 'identifier' and added it's docstrings --- .../workfile/abstract_template_loader.py | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 5d8d79397a..16287bbd4e 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -384,17 +384,11 @@ class AbstractPlaceholder: and assets to load. optional_keys: A list of optional keys to decribe placeholder and assets to load - loader: Name of linked loader to use while loading assets - is_context: Is placeholder linked - to context asset (or to linked assets) + loader_name: Name of linked loader to use while loading assets - Methods: - is_repres_valid: - loader: - order: - is_valid: - get_data: - parent_in_hierachy: + Args: + identifier (str): Placeholder identifier. Should be possible to be + used as identifier in "a scene" (e.g. unique node name). """ required_keys = { @@ -407,10 +401,10 @@ class AbstractPlaceholder: } optional_keys = {} - def __init__(self, node): + def __init__(self, identifier): self._log = None - self._name = node - self.get_data(node) + self._name = identifier + self.get_data(identifier) @property def log(self): From 5d0cd42a8133bcf7d65bbcef0c7b093ef058d7b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:47:01 +0200 Subject: [PATCH 067/155] renamed 'order' method to 'get_order' --- .../pipeline/workfile/abstract_template_loader.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 16287bbd4e..fe1f15c140 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -336,7 +336,7 @@ class AbstractTemplateLoader: placeholders = map(placeholder_class, self.get_template_nodes()) valid_placeholders = filter(placeholder_class.is_valid, placeholders) sorted_placeholders = sorted(valid_placeholders, - key=placeholder_class.order) + key=placeholder_class.get_order) return sorted_placeholders @abstractmethod @@ -427,17 +427,24 @@ class AbstractPlaceholder: def builder_type(self): return self.data["builder_type"] + @property def order(self): - """Get placeholder order. + return self.data["order"] + + def get_order(self): + """Placeholder order. + Order is used to sort them by priority Priority is lowset first, highest last (ex: 1: First to load 100: Last to load) + Returns: - Int: Order priority + int: Order priority """ - return self.data.get('order') + + return self.order @property def loader_name(self): From 7e8e61c0e4d51334d6de0d1f9cd672fa0dae5313 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:48:14 +0200 Subject: [PATCH 068/155] changed 'get_data' docstring --- openpype/pipeline/workfile/abstract_template_loader.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index fe1f15c140..66943eafe7 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -537,10 +537,12 @@ class AbstractPlaceholder: pass @abstractmethod - def get_data(self, node): - """ - Collect placeholders information. + def get_data(self, identifier): + """Collect information about placeholder by identifier. + Args: - node (AnyNode): A unique node decided by Placeholder implementation + identifier (str): A unique placeholder identifier defined by + implementation. """ + pass From a1cd1890d6db952e4feee357e204444aed0015ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:48:28 +0200 Subject: [PATCH 069/155] modified 'parent_in_hierarchy' docstring --- openpype/pipeline/workfile/abstract_template_loader.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 66943eafe7..a1629d9b79 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -491,13 +491,13 @@ class AbstractPlaceholder: return False @abstractmethod - def parent_in_hierarchy(self, containers): - """Place container in correct hierarchy - given by placeholder + def parent_in_hierarchy(self, container): + """Place loaded container in correct hierarchy given by placeholder + Args: - containers (String): Container name returned back by - placeholder's loader. + container (Dict[str, Any]): Loaded container created by loader. """ + pass @abstractmethod From 56150d4abb72d8b0025a7724e002eab792aa34a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:48:48 +0200 Subject: [PATCH 070/155] removed unused method 'convert_to_db_filters' --- .../pipeline/workfile/abstract_template_loader.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index a1629d9b79..c36e489017 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -523,19 +523,6 @@ class AbstractPlaceholder: pass - @abstractmethod - def convert_to_db_filters(self, current_asset, linked_asset): - """map current placeholder data as a db filter - args: - current_asset (String): Name of current asset in context - linked asset (list[String]) : Names of assets linked to - current asset in context - Returns: - dict: a dictionnary describing a filter to look for asset in - a database - """ - pass - @abstractmethod def get_data(self, identifier): """Collect information about placeholder by identifier. From 56bbbdbd583b51ba07bac08e753fb5a2050a768f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:49:20 +0200 Subject: [PATCH 071/155] removed unused import --- openpype/pipeline/workfile/abstract_template_loader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index c36e489017..725ab1dab3 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -1,8 +1,6 @@ import os from abc import ABCMeta, abstractmethod -import traceback - import six import logging from functools import reduce From 02edebad41f26680f0f7ceb3b2b21fe6cfebebab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Aug 2022 18:33:03 +0200 Subject: [PATCH 072/155] fix import string --- openpype/pipeline/workfile/build_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/build_template.py b/openpype/pipeline/workfile/build_template.py index df6fe3514a..e6396578c5 100644 --- a/openpype/pipeline/workfile/build_template.py +++ b/openpype/pipeline/workfile/build_template.py @@ -15,7 +15,7 @@ from .build_template_exceptions import ( MissingTemplateLoaderClass ) -_module_path_format = 'openpype.{host}.template_loader' +_module_path_format = 'openpype.hosts.{host}.api.template_loader' def build_workfile_template(*args): From 4f9d1c34e22d22729fc99fc92abcfaeb16ca253b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:39:27 +0200 Subject: [PATCH 073/155] added IHostModule to be able identify module representing a host --- openpype/modules/interfaces.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 334485cab2..424dd158fd 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -1,4 +1,4 @@ -from abc import abstractmethod +from abc import abstractmethod, abstractproperty from openpype import resources @@ -320,3 +320,13 @@ class ISettingsChangeListener(OpenPypeInterface): self, old_value, new_value, changes, project_name, new_value_metadata ): pass + + +class IHostModule(OpenPypeInterface): + """Module which also contain a host implementation.""" + + @abstractproperty + def host_name(self): + """Name of host which module represents.""" + + pass From c86ab4fecfbb6e502723bb86dbbf0748a8135753 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:40:08 +0200 Subject: [PATCH 074/155] added ability to inmport host modules on load modules --- openpype/modules/base.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1bd343fd07..32fa4d2f31 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -140,7 +140,7 @@ class _LoadCache: def get_default_modules_dir(): """Path to default OpenPype modules.""" - current_dir = os.path.abspath(os.path.dirname(__file__)) + current_dir = os.path.dirname(os.path.abspath(__file__)) output = [] for folder_name in ("default_modules", ): @@ -298,6 +298,8 @@ def _load_modules(): # Add current directory at first place # - has small differences in import logic current_dir = os.path.abspath(os.path.dirname(__file__)) + hosts_dir = os.path.join(os.path.dirname(current_dir), "hosts") + module_dirs.insert(0, hosts_dir) module_dirs.insert(0, current_dir) processed_paths = set() @@ -314,6 +316,7 @@ def _load_modules(): continue is_in_current_dir = dirpath == current_dir + is_in_host_dir = dirpath == hosts_dir for filename in os.listdir(dirpath): # Ignore filenames if filename in IGNORED_FILENAMES: @@ -353,6 +356,24 @@ def _load_modules(): sys.modules[new_import_str] = default_module setattr(openpype_modules, basename, default_module) + elif is_in_host_dir: + import_str = "openpype.hosts.{}".format(basename) + new_import_str = "{}.{}".format(modules_key, basename) + # Until all hosts are converted to be able use them as + # modules is this error check needed + try: + default_module = __import__( + import_str, fromlist=("", ) + ) + sys.modules[new_import_str] = default_module + setattr(openpype_modules, basename, default_module) + + except Exception: + log.warning( + "Failed to import host folder {}".format(basename), + exc_info=True + ) + elif os.path.isdir(fullpath): import_module_from_dirpath(dirpath, filename, modules_key) From 5736b9133cd8f2b2a62146cf6c9fb8310a74f4b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:40:32 +0200 Subject: [PATCH 075/155] added helper methods to be able get host module by host name --- openpype/modules/base.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 32fa4d2f31..ef577e5aa2 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -825,6 +825,45 @@ class ModulesManager: output.extend(hook_paths) return output + def get_host_module(self, host_name): + """Find host module by host name. + + Args: + host_name (str): Host name for which is found host module. + + Returns: + OpenPypeModule: Found host module by name. + None: There was not found module inheriting IHostModule which has + host name set to passed 'host_name'. + """ + + from openpype_interfaces import IHostModule + + for module in self.get_enabled_modules(): + if ( + isinstance(module, IHostModule) + and module.host_name == host_name + ): + return module + return None + + def get_host_names(self): + """List of available host names based on host modules. + + Returns: + Iterable[str]: All available host names based on enabled modules + inheriting 'IHostModule'. + """ + + from openpype_interfaces import IHostModule + + host_names = { + module.host_name + for module in self.get_enabled_modules() + if isinstance(module, IHostModule) + } + return host_names + def print_report(self): """Print out report of time spent on modules initialization parts. From a2dadc85bd51c2fc25baf5098ec5fdcf08e00269 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:42:54 +0200 Subject: [PATCH 076/155] added 'OpenPypeMaya' module --- openpype/hosts/maya/__init__.py | 7 +++++++ openpype/hosts/maya/module.py | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 openpype/hosts/maya/module.py diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index c1c82c62e5..2178534b89 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -1,4 +1,5 @@ import os +from .module import OpenPypeMaya def add_implementation_envs(env, _app): @@ -25,3 +26,9 @@ def add_implementation_envs(env, _app): for key, value in defaults.items(): if not env.get(key): env[key] = value + + +__all__ = ( + "OpenPypeMaya", + "add_implementation_envs", +) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py new file mode 100644 index 0000000000..8dfd96d4ab --- /dev/null +++ b/openpype/hosts/maya/module.py @@ -0,0 +1,10 @@ +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + + +class OpenPypeMaya(OpenPypeModule, IHostModule): + name = "openpype_maya" + host_name = "maya" + + def initialize(self, module_settings): + self.enabled = True From 88be0405986196894b16ae5cb98d303d3d0e9598 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:43:50 +0200 Subject: [PATCH 077/155] modev 'add_implementation_envs' to maya module and application knows that it should look there --- openpype/hosts/maya/__init__.py | 28 ---------------------------- openpype/hosts/maya/module.py | 27 +++++++++++++++++++++++++++ openpype/lib/applications.py | 6 ++++-- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index 2178534b89..72b4d5853c 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -1,34 +1,6 @@ -import os from .module import OpenPypeMaya -def add_implementation_envs(env, _app): - # Add requirements to PYTHONPATH - pype_root = os.environ["OPENPYPE_REPOS_ROOT"] - new_python_paths = [ - os.path.join(pype_root, "openpype", "hosts", "maya", "startup") - ] - old_python_path = env.get("PYTHONPATH") or "" - for path in old_python_path.split(os.pathsep): - if not path: - continue - - norm_path = os.path.normpath(path) - if norm_path not in new_python_paths: - new_python_paths.append(norm_path) - - env["PYTHONPATH"] = os.pathsep.join(new_python_paths) - - # Set default values if are not already set via settings - defaults = { - "OPENPYPE_LOG_NO_COLORS": "Yes" - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value - - __all__ = ( "OpenPypeMaya", - "add_implementation_envs", ) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index 8dfd96d4ab..0af68788bc 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -1,6 +1,9 @@ +import os from openpype.modules import OpenPypeModule from openpype.modules.interfaces import IHostModule +MAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + class OpenPypeMaya(OpenPypeModule, IHostModule): name = "openpype_maya" @@ -8,3 +11,27 @@ class OpenPypeMaya(OpenPypeModule, IHostModule): def initialize(self, module_settings): self.enabled = True + + def add_implementation_envs(self, env, _app): + # Add requirements to PYTHONPATH + new_python_paths = [ + os.path.join(MAYA_ROOT_DIR, "startup") + ] + old_python_path = env.get("PYTHONPATH") or "" + for path in old_python_path.split(os.pathsep): + if not path: + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_python_paths: + new_python_paths.append(norm_path) + + env["PYTHONPATH"] = os.pathsep.join(new_python_paths) + + # Set default values if are not already set via settings + defaults = { + "OPENPYPE_LOG_NO_COLORS": "Yes" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index da8623ea13..e47ec8cd11 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1508,8 +1508,10 @@ def prepare_app_environments( final_env = None # Add host specific environments if app.host_name and implementation_envs: - module = __import__("openpype.hosts", fromlist=[app.host_name]) - host_module = getattr(module, app.host_name, None) + host_module = modules_manager.get_host_module(app.host_name) + if not host_module: + module = __import__("openpype.hosts", fromlist=[app.host_name]) + host_module = getattr(module, app.host_name, None) add_implementation_envs = None if host_module: add_implementation_envs = getattr( From 32176ba234cf1bff28e15c4efce51cc00d641037 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:29:06 +0200 Subject: [PATCH 078/155] modules does not have to inherit from ILaunchHookPaths and application is passed to 'collect_launch_hook_paths --- openpype/lib/applications.py | 4 +++- openpype/modules/base.py | 38 ++++++++++++++++++++++++++++------ openpype/modules/interfaces.py | 33 ++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index e47ec8cd11..5443320960 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -996,7 +996,9 @@ class ApplicationLaunchContext: paths.append(path) # Load modules paths - paths.extend(self.modules_manager.collect_launch_hook_paths()) + paths.extend( + self.modules_manager.collect_launch_hook_paths(self.application) + ) return paths diff --git a/openpype/modules/base.py b/openpype/modules/base.py index ef577e5aa2..e26075283d 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -789,24 +789,50 @@ class ModulesManager: output.extend(paths) return output - def collect_launch_hook_paths(self): - """Helper to collect hooks from modules inherited ILaunchHookPaths. + def collect_launch_hook_paths(self, app): + """Helper to collect application launch hooks. + + It used to be based on 'ILaunchHookPaths' which is not true anymore. + Module just have to have implemented 'get_launch_hook_paths' method. + + Args: + app (Application): Application object which can be used for + filtering of which launch hook paths are returned. Returns: list: Paths to launch hook directories. """ - from openpype_interfaces import ILaunchHookPaths str_type = type("") expected_types = (list, tuple, set) output = [] for module in self.get_enabled_modules(): - # Skip module that do not inherit from `ILaunchHookPaths` - if not isinstance(module, ILaunchHookPaths): + # Skip module if does not have implemented 'get_launch_hook_paths' + func = getattr(module, "get_launch_hook_paths", None) + if func is None: + continue + + func = module.get_launch_hook_paths + if hasattr(inspect, "signature"): + sig = inspect.signature(func) + expect_args = len(sig.parameters) > 0 + else: + expect_args = len(inspect.getargspec(func)[0]) > 0 + + # Pass application argument if method expect it. + try: + if expect_args: + hook_paths = func(app) + else: + hook_paths = func() + except Exception: + self.log.warning( + "Failed to call 'get_launch_hook_paths'", + exc_info=True + ) continue - hook_paths = module.get_launch_hook_paths() if not hook_paths: continue diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 424dd158fd..de9ba13800 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -50,12 +50,32 @@ class IPluginPaths(OpenPypeInterface): class ILaunchHookPaths(OpenPypeInterface): """Module has launch hook paths to return. + Modules does not have to inherit from this interface (changed 8.11.2022). + Module just have to have implemented 'get_launch_hook_paths' to be able use + the advantage. + Expected result is list of paths. ["path/to/launch_hooks_dir"] """ @abstractmethod - def get_launch_hook_paths(self): + def get_launch_hook_paths(self, app): + """Paths to directory with application launch hooks. + + Method can be also defined without arguments. + ```python + def get_launch_hook_paths(self): + return [] + ``` + + Args: + app (Application): Application object which can be used for + filtering of which launch hook paths are returned. + + Returns: + Iterable[str]: Path to directories where launch hooks can be found. + """ + pass @@ -66,6 +86,7 @@ class ITrayModule(OpenPypeInterface): The module still must be usable if is not used in tray even if would do nothing. """ + tray_initialized = False _tray_manager = None @@ -78,16 +99,19 @@ class ITrayModule(OpenPypeInterface): This is where GUIs should be loaded or tray specific parts should be prepared. """ + pass @abstractmethod def tray_menu(self, tray_menu): """Add module's action to tray menu.""" + pass @abstractmethod def tray_start(self): """Start procedure in Pype tray.""" + pass @abstractmethod @@ -96,6 +120,7 @@ class ITrayModule(OpenPypeInterface): This is place where all threads should be shut. """ + pass def execute_in_main_thread(self, callback): @@ -104,6 +129,7 @@ class ITrayModule(OpenPypeInterface): Some callbacks need to be processed on main thread (menu actions must be added on main thread or they won't get triggered etc.) """ + if not self.tray_initialized: # TODO Called without initialized tray, still main thread needed try: @@ -128,6 +154,7 @@ class ITrayModule(OpenPypeInterface): msecs (int): Duration of message visibility in miliseconds. Default is 10000 msecs, may differ by Qt version. """ + if self._tray_manager: self._tray_manager.show_tray_message(title, message, icon, msecs) @@ -280,16 +307,19 @@ class ITrayService(ITrayModule): def set_service_running_icon(self): """Change icon of an QAction to green circle.""" + if self.menu_action: self.menu_action.setIcon(self.get_icon_running()) def set_service_failed_icon(self): """Change icon of an QAction to red circle.""" + if self.menu_action: self.menu_action.setIcon(self.get_icon_failed()) def set_service_idle_icon(self): """Change icon of an QAction to orange circle.""" + if self.menu_action: self.menu_action.setIcon(self.get_icon_idle()) @@ -303,6 +333,7 @@ class ISettingsChangeListener(OpenPypeInterface): "publish": ["path/to/publish_plugins"] } """ + @abstractmethod def on_system_settings_save( self, old_value, new_value, changes, new_value_metadata From 0ae844401cc271ef0edf9b16a5dda4893dd7bcfd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:29:35 +0200 Subject: [PATCH 079/155] maya is registering it's launch hooks --- openpype/hosts/maya/module.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index 0af68788bc..e058f1cef5 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -35,3 +35,10 @@ class OpenPypeMaya(OpenPypeModule, IHostModule): for key, value in defaults.items(): if not env.get(key): env[key] = value + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(MAYA_ROOT_DIR, "hooks") + ] From 58af54c4437d0495f2f00c7962455bd8cdbf1a1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:33:35 +0200 Subject: [PATCH 080/155] let host module add it's prelaunch hooks and don't guess it --- openpype/lib/applications.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 5443320960..e23cc6215f 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -962,32 +962,24 @@ class ApplicationLaunchContext: # TODO load additional studio paths from settings import openpype - pype_dir = os.path.dirname(os.path.abspath(openpype.__file__)) + openpype_dir = os.path.dirname(os.path.abspath(openpype.__file__)) - # --- START: Backwards compatibility --- - hooks_dir = os.path.join(pype_dir, "hooks") + global_hooks_dir = os.path.join(openpype_dir, "hooks") - subfolder_names = ["global"] - if self.host_name: - subfolder_names.append(self.host_name) - for subfolder_name in subfolder_names: - path = os.path.join(hooks_dir, subfolder_name) - if ( - os.path.exists(path) - and os.path.isdir(path) - and path not in paths - ): - paths.append(path) - # --- END: Backwards compatibility --- - - subfolders_list = [ - ["hooks"] + hooks_dirs = [ + global_hooks_dir ] if self.host_name: - subfolders_list.append(["hosts", self.host_name, "hooks"]) + # If host requires launch hooks and is module then launch hooks + # should be collected using 'collect_launch_hook_paths' + # - module have to implement 'get_launch_hook_paths' + host_module = self.modules_manager.get_host_module(self.host_name) + if not host_module: + hooks_dirs.append(os.path.join( + openpype_dir, "hosts", self.host_name, "hooks" + )) - for subfolders in subfolders_list: - path = os.path.join(pype_dir, *subfolders) + for path in hooks_dirs: if ( os.path.exists(path) and os.path.isdir(path) From 7d304d0f8695775e6f3e49d2b0271ac2b8564883 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:39:32 +0200 Subject: [PATCH 081/155] host module can define workfile extensions --- openpype/lib/applications.py | 23 ++++++++++++++++------- openpype/modules/interfaces.py | 11 +++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index e23cc6215f..0f380d0f4b 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1303,6 +1303,7 @@ def get_app_environments_for_context( dict: Environments for passed context and application. """ + from openpype.modules import ModulesManager from openpype.pipeline import AvalonMongoDB, Anatomy # Avalon database connection @@ -1315,8 +1316,6 @@ def get_app_environments_for_context( asset_doc = get_asset_by_name(project_name, asset_name) if modules_manager is None: - from openpype.modules import ModulesManager - modules_manager = ModulesManager() # Prepare app object which can be obtained only from ApplciationManager @@ -1343,7 +1342,7 @@ def get_app_environments_for_context( }) prepare_app_environments(data, env_group, modules_manager) - prepare_context_environments(data, env_group) + prepare_context_environments(data, env_group, modules_manager) # Discard avalon connection dbcon.uninstall() @@ -1564,7 +1563,7 @@ def apply_project_environments_value( return env -def prepare_context_environments(data, env_group=None): +def prepare_context_environments(data, env_group=None, modules_manager=None): """Modify launch environments with context data for launched host. Args: @@ -1652,10 +1651,10 @@ def prepare_context_environments(data, env_group=None): data["env"]["AVALON_APP"] = app.host_name data["env"]["AVALON_WORKDIR"] = workdir - _prepare_last_workfile(data, workdir) + _prepare_last_workfile(data, workdir, modules_manager) -def _prepare_last_workfile(data, workdir): +def _prepare_last_workfile(data, workdir, modules_manager): """last workfile workflow preparation. Function check if should care about last workfile workflow and tries @@ -1670,8 +1669,13 @@ def _prepare_last_workfile(data, workdir): result will be stored. workdir (str): Path to folder where workfiles should be stored. """ + + from openpype.modules import ModulesManager from openpype.pipeline import HOST_WORKFILE_EXTENSIONS + if not modules_manager: + modules_manager = ModulesManager() + log = data["log"] _workdir_data = data.get("workdir_data") @@ -1719,7 +1723,12 @@ def _prepare_last_workfile(data, workdir): # Last workfile path last_workfile_path = data.get("last_workfile_path") or "" if not last_workfile_path: - extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name) + host_module = modules_manager.get_host_module(app.host_name) + if host_module: + extensions = host_module.get_workfile_extensions() + else: + extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name) + if extensions: anatomy = data["anatomy"] project_settings = data["project_settings"] diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index de9ba13800..14f49204ee 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -361,3 +361,14 @@ class IHostModule(OpenPypeInterface): """Name of host which module represents.""" pass + + def get_workfile_extensions(self): + """Define workfile extensions for host. + + Not all hosts support workfiles thus this is optional implementation. + + Returns: + List[str]: Extensions used for workfiles with dot. + """ + + return [] From 9b623c1dd3e335aeb48d3428f6a0cba5e5793e51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 14:14:21 +0200 Subject: [PATCH 082/155] maya define it's workfile extensions only in module itself --- openpype/hosts/maya/api/workio.py | 4 +--- openpype/hosts/maya/module.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/workio.py b/openpype/hosts/maya/api/workio.py index fd4961c4bf..8c31974c73 100644 --- a/openpype/hosts/maya/api/workio.py +++ b/openpype/hosts/maya/api/workio.py @@ -2,11 +2,9 @@ import os from maya import cmds -from openpype.pipeline import HOST_WORKFILE_EXTENSIONS - def file_extensions(): - return HOST_WORKFILE_EXTENSIONS["maya"] + return [".ma", ".mb"] def has_unsaved_changes(): diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index e058f1cef5..5a215be8d2 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -42,3 +42,6 @@ class OpenPypeMaya(OpenPypeModule, IHostModule): return [ os.path.join(MAYA_ROOT_DIR, "hooks") ] + + def get_workfile_extensions(self): + return [".ma", ".mb"] From 25616886bff2b6fda0b4c9646ea9256389ba248f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:08:14 +0200 Subject: [PATCH 083/155] raise and error when nothing is selected --- openpype/hosts/maya/api/lib_template_builder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 855c72e361..34a8450a26 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,6 +40,9 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) + if not selection: + raise ValueError("Nothing is selected") + placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) From 683468c5633a42b8c5e80510ab060f981452d02c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:22:08 +0200 Subject: [PATCH 084/155] use 'filter_profiles' function for profiles filtering --- .../workfile/abstract_template_loader.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 725ab1dab3..51d06cdb3f 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -7,7 +7,11 @@ from functools import reduce from openpype.client import get_asset_by_name from openpype.settings import get_project_settings -from openpype.lib import get_linked_assets, PypeLogger as Logger +from openpype.lib import ( + Logger, + filter_profiles, + get_linked_assets, +) from openpype.pipeline import legacy_io, Anatomy from openpype.pipeline.load import ( get_loaders_by_name, @@ -167,22 +171,23 @@ class AbstractTemplateLoader: anatomy = Anatomy(project_name) project_settings = get_project_settings(project_name) - build_info = project_settings[host_name]['templated_workfile_build'] - profiles = build_info['profiles'] + build_info = project_settings[host_name]["templated_workfile_build"] + profile = filter_profiles( + build_info["profiles"], + { + "task_types": task_type, + "tasks": task_name + } + ) - for prf in profiles: - if prf['task_types'] and task_type not in prf['task_types']: - continue - if prf['tasks'] and task_name not in prf['tasks']: - continue - path = prf['path'] - break - else: # IF no template were found (no break happened) + if not profile: raise TemplateProfileNotFound( "No matching profile found for task '{}' of type '{}' " "with host '{}'".format(task_name, task_type, host_name) ) - if path is None: + + path = profile["path"] + if not path: raise TemplateLoadingFailed( "Template path is not set.\n" "Path need to be set in {}\\Template Workfile Build " From bb9a16100acd9a7d94ec6ff6ea15891916eea580 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:22:23 +0200 Subject: [PATCH 085/155] removed unnecessary finally statement --- openpype/pipeline/workfile/abstract_template_loader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 51d06cdb3f..0ed32033af 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -205,9 +205,8 @@ class AbstractTemplateLoader: raise KeyError( "Could not solve key '{}' in template path '{}'".format( missing_key, path)) - finally: - solved_path = os.path.normpath(solved_path) + solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): raise TemplateNotFound( "Template found in openPype settings for task '{}' with host " From 12a8307a8331334ee9700efba2127211ea332ff0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:40:02 +0200 Subject: [PATCH 086/155] simplified path formatting --- .../workfile/abstract_template_loader.py | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 0ed32033af..5afec56d71 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -8,6 +8,7 @@ from functools import reduce from openpype.client import get_asset_by_name from openpype.settings import get_project_settings from openpype.lib import ( + StringTemplate, Logger, filter_profiles, get_linked_assets, @@ -192,19 +193,35 @@ class AbstractTemplateLoader: "Template path is not set.\n" "Path need to be set in {}\\Template Workfile Build " "Settings\\Profiles".format(host_name.title())) - try: - solved_path = None - while True: + + # Try fill path with environments and anatomy roots + fill_data = { + key: value + for key, value in os.environ.items() + } + fill_data["root"] = anatomy.roots + result = StringTemplate.format_template(path, fill_data) + if result.solved: + path = result.normalized() + + if path and os.path.exists(path): + self.log.info("Found template at: '{}'".format(path)) + return path + + solved_path = None + while True: + try: solved_path = anatomy.path_remapper(path) - if solved_path is None: - solved_path = path - if solved_path == path: - break - path = solved_path - except KeyError as missing_key: - raise KeyError( - "Could not solve key '{}' in template path '{}'".format( - missing_key, path)) + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): @@ -213,7 +230,7 @@ class AbstractTemplateLoader: "'{}' does not exists. (Not found : {})".format( task_name, host_name, solved_path)) - self.log.info("Found template at : '{}'".format(solved_path)) + self.log.info("Found template at: '{}'".format(solved_path)) return solved_path From 748dcf1ad207edd3dbf3bc98120d9e46bf9b39e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:51:27 +0200 Subject: [PATCH 087/155] fix filter and sort --- .../workfile/abstract_template_loader.py | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 5afec56d71..1c8ede25e6 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -351,11 +351,15 @@ class AbstractTemplateLoader: self.populate_template(ignored_ids=loaded_containers_ids) def get_placeholders(self): - placeholder_class = self.placeholder_class - placeholders = map(placeholder_class, self.get_template_nodes()) - valid_placeholders = filter(placeholder_class.is_valid, placeholders) - sorted_placeholders = sorted(valid_placeholders, - key=placeholder_class.get_order) + placeholders = map(self.placeholder_class, self.get_template_nodes()) + valid_placeholders = filter( + lambda i: i.is_valid, + placeholders + ) + sorted_placeholders = list(sorted( + valid_placeholders, + key=lambda i: i.order + )) return sorted_placeholders @abstractmethod @@ -450,21 +454,6 @@ class AbstractPlaceholder: def order(self): return self.data["order"] - def get_order(self): - """Placeholder order. - - Order is used to sort them by priority - Priority is lowset first, highest last - (ex: - 1: First to load - 100: Last to load) - - Returns: - int: Order priority - """ - - return self.order - @property def loader_name(self): """Return placeholder loader type. From 7eaa278c741ceaa30daf056dd17ec9e4b4ceed10 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:54:19 +0200 Subject: [PATCH 088/155] removed invalid default setting for templates --- openpype/settings/defaults/project_settings/maya.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 9c2c737ece..e9109abd22 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -123,7 +123,6 @@ "defaults": [ "Main" ] - }, "CreateAss": { "enabled": true, @@ -969,13 +968,7 @@ ] }, "templated_workfile_build": { - "profiles": [ - { - "task_types": [], - "tasks": [], - "path": "/path/to/your/template" - } - ] + "profiles": [] }, "filters": { "preset 1": { From 66ee0beaf6d0e09eae6a8a9887a90651618a73f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 17:43:47 +0200 Subject: [PATCH 089/155] fix empty or query --- openpype/client/entities.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index f9d3badb1a..c798c0ad6d 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1130,11 +1130,12 @@ def _get_representations( for item in _regex_filters(flatten_filters): for key, value in item.items(): - if key == "$or": - or_queries.append(value) - else: + 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: From cd167a9055723c941e34c15fd3d5cc8edbaf481e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 11 Aug 2022 18:00:34 +0200 Subject: [PATCH 090/155] Removed submodule vendor/configs/OpenColorIO-Configs --- .gitmodules | 5 +---- vendor/configs/OpenColorIO-Configs | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/.gitmodules b/.gitmodules index bac3132b77..fe93791c4e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,7 +4,4 @@ [submodule "tools/modules/powershell/PSWriteColor"] path = tools/modules/powershell/PSWriteColor - url = https://github.com/EvotecIT/PSWriteColor.git -[submodule "vendor/configs/OpenColorIO-Configs"] - path = vendor/configs/OpenColorIO-Configs - url = https://github.com/imageworks/OpenColorIO-Configs + url = https://github.com/EvotecIT/PSWriteColor.git \ No newline at end of file diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08b..0000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 37ed6bc897168e42159fb656f06d413b11c601da Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 11 Aug 2022 18:02:21 +0200 Subject: [PATCH 091/155] :recycle: change location of ocio configs --- .../maya/plugins/publish/extract_look.py | 18 +- poetry.lock | 821 +++--------------- pyproject.toml | 4 +- 3 files changed, 131 insertions(+), 712 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 0b26e922d5..b425efba6f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -40,15 +40,15 @@ def get_ocio_config_path(profile_folder): Returns: str: Path to vendorized config file. """ - return os.path.join( - os.environ["OPENPYPE_ROOT"], - "vendor", - "configs", - "OpenColorIO-Configs", - profile_folder, - "config.ocio" - ) - + try: + import OpenColorIOConfigs + return os.path.join( + os.path.dirname(OpenColorIOConfigs.__file__), + profile_folder, + "config.ocio" + ) + except ImportError: + return None def find_paths_by_hash(texture_hash): """Find the texture hash key in the dictionary. diff --git a/poetry.lock b/poetry.lock index 919a352505..df8d8ab14a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -48,7 +48,7 @@ aiohttp = ">=3,<4" [[package]] name = "aiohttp-middlewares" -version = "2.0.0" +version = "2.1.0" description = "Collection of useful middlewares for aiohttp applications." category = "main" optional = false @@ -114,7 +114,7 @@ python-dateutil = ">=2.7.0" [[package]] name = "astroid" -version = "2.11.5" +version = "2.11.7" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -147,7 +147,7 @@ python-versions = ">=3.5" [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false @@ -155,17 +155,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "autopep8" @@ -181,11 +181,11 @@ toml = "*" [[package]] name = "babel" -version = "2.9.1" +version = "2.10.3" description = "Internationalization utilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] pytz = ">=2015.7" @@ -236,7 +236,7 @@ python-versions = ">=3.6" [[package]] name = "cffi" -version = "1.15.0" +version = "1.15.1" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -279,7 +279,7 @@ test = ["pytest-runner (>=2.7,<3)", "pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)" [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "dev" optional = false @@ -306,7 +306,7 @@ python-versions = "*" [[package]] name = "coverage" -version = "6.4.1" +version = "6.4.3" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -320,7 +320,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "37.0.2" +version = "37.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -408,7 +408,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "dropbox" -version = "11.31.0" +version = "11.33.0" description = "Official Dropbox API Client" category = "main" optional = false @@ -433,7 +433,7 @@ prefixed = ">=0.3.2" [[package]] name = "evdev" -version = "1.5.0" +version = "1.6.0" description = "Bindings to the Linux input handling subsystem" category = "main" optional = false @@ -455,7 +455,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "frozenlist" -version = "1.3.0" +version = "1.3.1" description = "A list-like structure which implements collections.abc.MutableSequence" category = "main" optional = false @@ -490,7 +490,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "gazu" -version = "0.8.28" +version = "0.8.30" description = "Gazu is a client for Zou, the API to store the data of your CG production." category = "main" optional = false @@ -530,7 +530,7 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\"" [[package]] name = "google-api-core" -version = "2.8.1" +version = "2.8.2" description = "Google API client core library" category = "main" optional = false @@ -539,13 +539,11 @@ python-versions = ">=3.6" [package.dependencies] google-auth = ">=1.25.0,<3.0dev" googleapis-common-protos = ">=1.56.2,<2.0dev" -protobuf = ">=3.15.0,<4.0.0dev" +protobuf = ">=3.15.0,<5.0.0dev" requests = ">=2.18.0,<3.0.0dev" [package.extras] grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)"] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] [[package]] name = "google-api-python-client" @@ -565,7 +563,7 @@ uritemplate = ">=3.0.0,<4dev" [[package]] name = "google-auth" -version = "2.7.0" +version = "2.10.0" description = "Google Authentication Library" category = "main" optional = false @@ -598,14 +596,14 @@ six = "*" [[package]] name = "googleapis-common-protos" -version = "1.56.2" +version = "1.56.4" description = "Common protobufs used in Google APIs" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -protobuf = ">=3.15.0,<4.0.0dev" +protobuf = ">=3.15.0,<5.0.0dev" [package.extras] grpc = ["grpcio (>=1.0.0,<2.0.0dev)"] @@ -631,7 +629,7 @@ python-versions = ">=3.5" [[package]] name = "imagesize" -version = "1.3.0" +version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "dev" optional = false @@ -639,7 +637,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.11.4" +version = "4.12.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -652,7 +650,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -692,15 +690,15 @@ testing = ["colorama", "docopt", "pytest (>=3.1.0)"] [[package]] name = "jeepney" -version = "0.7.1" +version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] -test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] -trio = ["trio", "async-generator"] +trio = ["async-generator", "trio"] +test = ["async-timeout", "trio", "testpath", "pytest-asyncio (>=0.17)", "pytest-trio", "pytest"] [[package]] name = "jinja2" @@ -799,6 +797,21 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "opencolorio-configs" +version = "1.0.2" +description = "Curated set of OpenColorIO Configs for use in OpenPype" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.source] +type = "git" +url = "https://github.com/pypeclub/OpenColorIO-Configs.git" +reference = "main" +resolved_reference = "07c5e865bf2b115b589dd2876ae632cd410821b5" + [[package]] name = "opentimelineio" version = "0.14.0.dev1" @@ -875,14 +888,14 @@ six = "*" [[package]] name = "pillow" -version = "9.1.1" +version = "9.2.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "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]] @@ -930,11 +943,11 @@ python-versions = "*" [[package]] name = "protobuf" -version = "3.19.4" -description = "Protocol Buffers" +version = "4.21.5" +description = "" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [[package]] name = "py" @@ -1354,7 +1367,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "rsa" -version = "4.8" +version = "4.9" description = "Pure-Python RSA implementation" category = "main" optional = false @@ -1408,7 +1421,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "slack-sdk" -version = "3.17.0" +version = "3.18.1" description = "The Slack API Platform SDK for Python" category = "main" optional = false @@ -1487,9 +1500,9 @@ docutils = "*" sphinx = "*" [package.extras] +test = ["pytest-cov", "pytest (>=3.0.0)"] +lint = ["pylint", "flake8", "black"] dev = ["pre-commit"] -lint = ["black", "flake8", "pylint"] -test = ["pytest (>=3.0.0)", "pytest-cov"] [[package]] name = "sphinx-rtd-theme" @@ -1638,11 +1651,11 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "uritemplate" @@ -1654,11 +1667,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "urllib3" -version = "1.26.9" +version = "1.26.11" 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.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] @@ -1711,11 +1724,11 @@ ujson = ["ujson"] [[package]] name = "yarl" -version = "1.7.2" +version = "1.8.1" description = "Yet another URL library" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] idna = ">=2.0" @@ -1724,20 +1737,20 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.8.0" +version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "bd8e0a03668c380c6e76c8cd6c71020692f4ea9f32de7a4f09433564faa9dad0" +content-hash = "89fb7e8ad310b5048bf78561f1146194c8779e286d839cc000f04e88be87f3f3" [metadata.files] acre = [] @@ -1819,10 +1832,7 @@ aiohttp-json-rpc = [ {file = "aiohttp-json-rpc-0.13.3.tar.gz", hash = "sha256:6237a104478c22c6ef96c7227a01d6832597b414e4b79a52d85593356a169e99"}, {file = "aiohttp_json_rpc-0.13.3-py3-none-any.whl", hash = "sha256:4fbd197aced61bd2df7ae3237ead7d3e08833c2ccf48b8581e1828c95ebee680"}, ] -aiohttp-middlewares = [ - {file = "aiohttp-middlewares-2.0.0.tar.gz", hash = "sha256:e08ba04dc0e8fe379aa5e9444a68485c275677ee1e18c55cbb855de0c3629502"}, - {file = "aiohttp_middlewares-2.0.0-py3-none-any.whl", hash = "sha256:29cf1513176b4013844711975ff520e26a8a5d8f9fefbbddb5e91224a86b043e"}, -] +aiohttp-middlewares = [] aiosignal = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, @@ -1840,10 +1850,7 @@ arrow = [ {file = "arrow-0.17.0-py2.py3-none-any.whl", hash = "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5"}, {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"}, ] -astroid = [ - {file = "astroid-2.11.5-py3-none-any.whl", hash = "sha256:14ffbb4f6aa2cf474a0834014005487f7ecd8924996083ab411e7fa0b508ce0b"}, - {file = "astroid-2.11.5.tar.gz", hash = "sha256:f4e4ec5294c4b07ac38bab9ca5ddd3914d4bf46f9006eb5c0ae755755061044e"}, -] +astroid = [] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, @@ -1852,99 +1859,21 @@ asynctest = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, ] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] +atomicwrites = [] +attrs = [] autopep8 = [ {file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"}, {file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"}, ] -babel = [ - {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, - {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, -] -bcrypt = [ - {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, - {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, - {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, - {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, -] +babel = [] +bcrypt = [] blessed = [ {file = "blessed-1.19.1-py2.py3-none-any.whl", hash = "sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b"}, {file = "blessed-1.19.1.tar.gz", hash = "sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc"}, ] -cachetools = [ - {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"}, - {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, -] -certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, -] -cffi = [ - {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, - {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, - {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, - {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, - {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, - {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, - {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, - {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, - {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, - {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, - {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, - {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, - {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, - {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, - {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, - {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, - {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, -] +cachetools = [] +certifi = [] +cffi = [] charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, @@ -1957,10 +1886,7 @@ clique = [ {file = "clique-1.6.1-py2.py3-none-any.whl", hash = "sha256:8619774fa035661928dd8c93cd805acf2d42533ccea1b536c09815ed426c9858"}, {file = "clique-1.6.1.tar.gz", hash = "sha256:90165c1cf162d4dd1baef83ceaa1afc886b453e379094fa5b60ea470d1733e66"}, ] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] +colorama = [] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, @@ -1969,73 +1895,8 @@ coolname = [ {file = "coolname-1.1.0-py2.py3-none-any.whl", hash = "sha256:e6a83a0ac88640f4f3d2070438dbe112fe80cfebc119c93bd402976ec84c0978"}, {file = "coolname-1.1.0.tar.gz", hash = "sha256:410fe6ea9999bf96f2856ef0c726d5f38782bbefb7bb1aca0e91e0dc98ed09e3"}, ] -coverage = [ - {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, - {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, - {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, - {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, - {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, - {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, - {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, - {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, - {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, - {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, - {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, - {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, - {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, -] -cryptography = [ - {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"}, - {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"}, - {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"}, - {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"}, - {file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"}, - {file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"}, - {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"}, - {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"}, - {file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"}, -] +coverage = [] +cryptography = [] cx-freeze = [ {file = "cx_Freeze-6.9-cp310-cp310-win32.whl", hash = "sha256:776d4fb68a4831691acbd3c374362b9b48ce2e568514a73c3d4cb14d5dcf1470"}, {file = "cx_Freeze-6.9-cp310-cp310-win_amd64.whl", hash = "sha256:243f36d35a034a409cd6247d8cb5d1fbfd7374e3e668e813d0811f64d6bd5ed3"}, @@ -2064,14 +1925,8 @@ cx-logging = [ {file = "cx_Logging-3.0-cp39-cp39-win_amd64.whl", hash = "sha256:302e9c4f65a936c288a4fa59a90e7e142d9ef994aa29676731acafdcccdbb3f5"}, {file = "cx_Logging-3.0.tar.gz", hash = "sha256:ba8a7465facf7b98d8f494030fb481a2e8aeee29dc191e10383bb54ed42bdb34"}, ] -deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, -] -dill = [ - {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, - {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, -] +deprecated = [] +dill = [] dnspython = [ {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, @@ -2080,90 +1935,22 @@ docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] -dropbox = [ - {file = "dropbox-11.31.0-py2-none-any.whl", hash = "sha256:393a99dfe30d42fd73c265b9b7d24bb21c9a961739cd097c3541e709eb2a209c"}, - {file = "dropbox-11.31.0-py3-none-any.whl", hash = "sha256:5f924102fd6464def81573320c6aa4ea9cd3368e1b1c13d838403dd4c9ffc919"}, - {file = "dropbox-11.31.0.tar.gz", hash = "sha256:f483d65b702775b9abf7b9328f702c68c6397fc01770477c6ddbfb1d858a5bcf"}, -] +dropbox = [] enlighten = [ {file = "enlighten-1.10.2-py2.py3-none-any.whl", hash = "sha256:b237fe562b320bf9f1d4bb76d0c98e0daf914372a76ab87c35cd02f57aa9d8c1"}, {file = "enlighten-1.10.2.tar.gz", hash = "sha256:7a5b83cd0f4d095e59d80c648ebb5f7ffca0cd8bcf7ae6639828ee1ad000632a"}, ] -evdev = [ - {file = "evdev-1.5.0.tar.gz", hash = "sha256:5b33b174f7c84576e7dd6071e438bf5ad227da95efd4356a39fe4c8355412fe6"}, -] +evdev = [] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] -frozenlist = [ - {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, - {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, - {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, - {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, - {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, - {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, - {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, - {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, - {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, - {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, - {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, - {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, - {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, -] +frozenlist = [] ftrack-python-api = [] future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] -gazu = [ - {file = "gazu-0.8.28-py2.py3-none-any.whl", hash = "sha256:ec4f7c2688a2b37ee8a77737e4e30565ad362428c3ade9046136a998c043e51c"}, -] +gazu = [] gitdb = [ {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, @@ -2172,42 +1959,24 @@ gitpython = [ {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"}, {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, ] -google-api-core = [ - {file = "google-api-core-2.8.1.tar.gz", hash = "sha256:958024c6aa3460b08f35741231076a4dd9a4c819a6a39d44da9627febe8b28f0"}, - {file = "google_api_core-2.8.1-py3-none-any.whl", hash = "sha256:ce1daa49644b50398093d2a9ad886501aa845e2602af70c3001b9f402a9d7359"}, -] +google-api-core = [] google-api-python-client = [ {file = "google-api-python-client-1.12.11.tar.gz", hash = "sha256:1b4bd42a46321e13c0542a9e4d96fa05d73626f07b39f83a73a947d70ca706a9"}, {file = "google_api_python_client-1.12.11-py2.py3-none-any.whl", hash = "sha256:7e0a1a265c8d3088ee1987778c72683fcb376e32bada8d7767162bd9c503fd9b"}, ] -google-auth = [ - {file = "google-auth-2.7.0.tar.gz", hash = "sha256:8a954960f852d5f19e6af14dd8e75c20159609e85d8db37e4013cc8c3824a7e1"}, - {file = "google_auth-2.7.0-py2.py3-none-any.whl", hash = "sha256:df549a1433108801b11bdcc0e312eaf0d5f0500db42f0523e4d65c78722e8475"}, -] +google-auth = [] google-auth-httplib2 = [ {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, ] -googleapis-common-protos = [ - {file = "googleapis-common-protos-1.56.2.tar.gz", hash = "sha256:b09b56f5463070c2153753ef123f07d2e49235e89148e9b2459ec8ed2f68d7d3"}, - {file = "googleapis_common_protos-1.56.2-py2.py3-none-any.whl", hash = "sha256:023eaea9d8c1cceccd9587c6af6c20f33eeeb05d4148670f2b0322dc1511700c"}, -] +googleapis-common-protos = [] httplib2 = [ {file = "httplib2-0.20.4-py3-none-any.whl", hash = "sha256:8b6a905cb1c79eefd03f8669fd993c36dc341f7c558f056cb5a33b5c2f458543"}, {file = "httplib2-0.20.4.tar.gz", hash = "sha256:58a98e45b4b1a48273073f905d2961666ecf0fbac4250ea5b47aef259eb5c585"}, ] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -imagesize = [ - {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, - {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"}, - {file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"}, -] +idna = [] +imagesize = [] +importlib-metadata = [] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -2220,18 +1989,12 @@ jedi = [ {file = "jedi-0.13.3-py2.py3-none-any.whl", hash = "sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c"}, {file = "jedi-0.13.3.tar.gz", hash = "sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b"}, ] -jeepney = [ - {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, - {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, -] +jeepney = [] jinja2 = [ {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, ] -jinxed = [ - {file = "jinxed-1.2.0-py2.py3-none-any.whl", hash = "sha256:cfc2b2e4e3b4326954d546ba6d6b9a7a796ddcb0aef8d03161d005177eb0d48b"}, - {file = "jinxed-1.2.0.tar.gz", hash = "sha256:032acda92d5c57cd216033cbbd53de731e6ed50deb63eb4781336ca55f72cda5"}, -] +jinxed = [] jsonschema = [ {file = "jsonschema-2.6.0-py2.py3-none-any.whl", hash = "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08"}, {file = "jsonschema-2.6.0.tar.gz", hash = "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"}, @@ -2283,28 +2046,12 @@ log4mongo = [ {file = "log4mongo-1.7.0.tar.gz", hash = "sha256:dc374617206162a0b14167fbb5feac01dbef587539a235dadba6200362984a68"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2313,27 +2060,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2343,12 +2077,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2418,15 +2146,13 @@ multidict = [ {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, ] +opencolorio-configs = [] opentimelineio = [] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] -paramiko = [ - {file = "paramiko-2.11.0-py2.py3-none-any.whl", hash = "sha256:655f25dc8baf763277b933dfcea101d636581df8d6b9774d1fb653426b72c270"}, - {file = "paramiko-2.11.0.tar.gz", hash = "sha256:003e6bee7c034c21fbb051bf83dc0a9ee4106204dd3c53054c71452cc4ec3938"}, -] +paramiko = [] parso = [ {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, @@ -2435,50 +2161,8 @@ pathlib2 = [ {file = "pathlib2-2.3.7.post1-py2.py3-none-any.whl", hash = "sha256:5266a0fd000452f1b3467d782f079a4343c63aaa119221fbdc4e39577489ca5b"}, {file = "pathlib2-2.3.7.post1.tar.gz", hash = "sha256:9fe0edad898b83c0c3e199c842b27ed216645d2e177757b2dd67384d4113c641"}, ] -pillow = [ - {file = "Pillow-9.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe"}, - {file = "Pillow-9.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e"}, - {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602"}, - {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530"}, - {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2"}, - {file = "Pillow-9.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a"}, - {file = "Pillow-9.1.1-cp310-cp310-win32.whl", hash = "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c"}, - {file = "Pillow-9.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108"}, - {file = "Pillow-9.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b"}, - {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d"}, - {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84"}, - {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4"}, - {file = "Pillow-9.1.1-cp37-cp37m-win32.whl", hash = "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578"}, - {file = "Pillow-9.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9"}, - {file = "Pillow-9.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf"}, - {file = "Pillow-9.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b"}, - {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546"}, - {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f"}, - {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a"}, - {file = "Pillow-9.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1"}, - {file = "Pillow-9.1.1-cp38-cp38-win32.whl", hash = "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54"}, - {file = "Pillow-9.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf"}, - {file = "Pillow-9.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92"}, - {file = "Pillow-9.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a"}, - {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251"}, - {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d"}, - {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1"}, - {file = "Pillow-9.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601"}, - {file = "Pillow-9.1.1-cp39-cp39-win32.whl", hash = "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45"}, - {file = "Pillow-9.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c"}, - {file = "Pillow-9.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6"}, - {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098"}, - {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c"}, - {file = "Pillow-9.1.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd"}, - {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340"}, - {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765"}, - {file = "Pillow-9.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8"}, - {file = "Pillow-9.1.1.tar.gz", hash = "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] +pillow = [] +platformdirs = [] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, @@ -2491,34 +2175,7 @@ prefixed = [ {file = "prefixed-0.3.2-py2.py3-none-any.whl", hash = "sha256:5e107306462d63f2f03c529dbf11b0026fdfec621a9a008ca639d71de22995c3"}, {file = "prefixed-0.3.2.tar.gz", hash = "sha256:ca48277ba5fa8346dd4b760847da930c7b84416387c39e93affef086add2c029"}, ] -protobuf = [ - {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, - {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, - {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, - {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, - {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, - {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, - {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, - {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, - {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, - {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, - {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, - {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, - {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, - {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, - {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, - {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, - {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, - {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, -] +protobuf = [] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -2577,14 +2234,8 @@ pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] -pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, -] -pylint = [ - {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"}, - {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"}, -] +pygments = [] +pylint = [] pymongo = [ {file = "pymongo-3.12.3-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:c164eda0be9048f83c24b9b2656900041e069ddf72de81c17d874d0c32f6079f"}, {file = "pymongo-3.12.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:a055d29f1302892a9389a382bed10a3f77708bcf3e49bfb76f7712fa5f391cc6"}, @@ -2711,42 +2362,10 @@ pynput = [ {file = "pynput-1.7.6-py3.9.egg", hash = "sha256:264429fbe676e98e9050ad26a7017453bdd08768adb25cafb918347cf9f1eb4a"}, {file = "pynput-1.7.6.tar.gz", hash = "sha256:3a5726546da54116b687785d38b1db56997ce1d28e53e8d22fc656d8b92e533c"}, ] -pyobjc-core = [ - {file = "pyobjc-core-8.5.tar.gz", hash = "sha256:704c275439856c0d1287469f0d589a7d808d48b754a93d9ce5415d4eaf06d576"}, - {file = "pyobjc_core-8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0c234143b48334443f5adcf26e668945a6d47bc1fa6223e80918c6c735a029d9"}, - {file = "pyobjc_core-8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1486ee533f0d76f666804ce89723ada4db56bfde55e56151ba512d3f849857f8"}, - {file = "pyobjc_core-8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:412de06dfa728301c04b3e46fd7453320a8ae8b862e85236e547cd797a73b490"}, - {file = "pyobjc_core-8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b3e09cccb1be574a82cc9f929ae27fc4283eccc75496cb5d51534caa6bb83a3"}, - {file = "pyobjc_core-8.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:eeafe21f879666ab7f57efcc6b007c9f5f8733d367b7e380c925203ed83f000d"}, - {file = "pyobjc_core-8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c0071686976d7ea8c14690950e504a13cb22b4ebb2bc7b5ec47c1c1c0f6eff41"}, -] -pyobjc-framework-applicationservices = [ - {file = "pyobjc-framework-ApplicationServices-8.5.tar.gz", hash = "sha256:fa3015ef8e3add90af3447d7fdcc7f8dd083cc2a1d58f99a569480a2df10d2b1"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:436b16ebe448a829a8312e10208eec81a2adcae1fff674dbcc3262e1bd76e0ca"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:408958d14aa7fcf46f2163754c211078bc63be1368934d86188202914dce077d"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1d6cd4ce192859a22e208da4d7177a1c3ceb1ef2f64c339fd881102b1210cadd"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0251d092adb1d2d116fd9f147ceef0e53b158a46c21245131c40b9d7b786d0db"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:9742e69fe6d4545d0e02b0ad0a7a2432bc9944569ee07d6e90ffa5ef614df9f7"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16f5677c14ea903c6aaca1dd121521825c39e816cae696d6ae32c0b287252ab2"}, -] -pyobjc-framework-cocoa = [ - {file = "pyobjc-framework-Cocoa-8.5.tar.gz", hash = "sha256:569bd3a020f64b536fb2d1c085b37553e50558c9f907e08b73ffc16ae68e1861"}, - {file = "pyobjc_framework_Cocoa-8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7a7c160416696bf6035dfcdf0e603aaa52858d6afcddfcc5ab41733619ac2529"}, - {file = "pyobjc_framework_Cocoa-8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6ceba444282030be8596b812260e8d28b671254a51052ad778d32da6e17db847"}, - {file = "pyobjc_framework_Cocoa-8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f46b2b161b8dd40c7b9e00bc69636c3e6480b2704a69aee22ee0154befbe163a"}, - {file = "pyobjc_framework_Cocoa-8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b31d425aee8698cbf62b187338f5ca59427fa4dca2153a73866f7cb410713119"}, - {file = "pyobjc_framework_Cocoa-8.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:898359ac1f76eedec8aa156847682378a8950824421c40edb89391286e607dc4"}, - {file = "pyobjc_framework_Cocoa-8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:baa2947f76b119a3360973d74d57d6dada87ac527bab9a88f31596af392f123c"}, -] -pyobjc-framework-quartz = [ - {file = "pyobjc-framework-Quartz-8.5.tar.gz", hash = "sha256:d2bc5467a792ddc04814f12a1e9c2fcaf699a1c3ad3d4264cfdce6b9c7b10624"}, - {file = "pyobjc_framework_Quartz-8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e9f0fb663f7872c9de94169031ac42b91ad01bd4cad49a9f1a0164be8f028426"}, - {file = "pyobjc_framework_Quartz-8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:567eec91287cfe9a1b6433717192c585935de8f3daa28d82ce72fdd6c7ac00f6"}, - {file = "pyobjc_framework_Quartz-8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f910ab41a712ffc7a8c3e3716a2d6f39ea4419004b26a2fd2d2f740ff5c262c"}, - {file = "pyobjc_framework_Quartz-8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29d07066781628278bf0e5278abcfc96ef6724c66c5629a0b4c214d319a82e55"}, - {file = "pyobjc_framework_Quartz-8.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:72abcde1a3d72be11f2c881c9b9872044c8f2de86d2047b67fe771713638b107"}, - {file = "pyobjc_framework_Quartz-8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8809b9a2df2f461697bdb45b6d1b5a4f881f88f09450e3990858e64e3e26c530"}, -] +pyobjc-core = [] +pyobjc-framework-applicationservices = [] +pyobjc-framework-cocoa = [] +pyobjc-framework-quartz = [] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, @@ -2770,14 +2389,8 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] -python-engineio = [ - {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"}, -] -python-socketio = [ - {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"}, -] +python-engineio = [] +python-socketio = [] python-xlib = [ {file = "python-xlib-0.31.tar.gz", hash = "sha256:74d83a081f532bc07f6d7afcd6416ec38403d68f68b9b9dc9e1f28fbf2d799e9"}, {file = "python_xlib-0.31-py2.py3-none-any.whl", hash = "sha256:1ec6ce0de73d9e6592ead666779a5732b384e5b8fb1f1886bd0a81cafa477759"}, @@ -2805,10 +2418,7 @@ pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] -"qt.py" = [ - {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"}, -] +"qt.py" = [] qtawesome = [ {file = "QtAwesome-0.7.3-py2.py3-none-any.whl", hash = "sha256:ddf4530b4af71cec13b24b88a4cdb56ec85b1e44c43c42d0698804c7137b09b0"}, {file = "QtAwesome-0.7.3.tar.gz", hash = "sha256:b98b9038d19190e83ab26d91c4d8fc3a36591ee2bc7f5016d4438b8240d097bd"}, @@ -2821,18 +2431,9 @@ recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] -requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, -] -rsa = [ - {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, - {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, -] -secretstorage = [ - {file = "SecretStorage-3.3.2-py3-none-any.whl", hash = "sha256:755dc845b6ad76dcbcbc07ea3da75ae54bb1ea529eb72d15f83d26499a5df319"}, - {file = "SecretStorage-3.3.2.tar.gz", hash = "sha256:0a8eb9645b320881c222e827c26f4cfcf55363e8b374a021981ef886657a912f"}, -] +requests = [] +rsa = [] +secretstorage = [] semver = [ {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, @@ -2842,10 +2443,7 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -slack-sdk = [ - {file = "slack_sdk-3.17.0-py2.py3-none-any.whl", hash = "sha256:0816efc43d1d2db8286e8dbcbb2e86fd0f71c206c01c521c2cb054ecb40f9ced"}, - {file = "slack_sdk-3.17.0.tar.gz", hash = "sha256:860cd0e50c454b955f14321c8c5486a47cc1e0e84116acdb009107f836752feb"}, -] +slack-sdk = [] smmap = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, @@ -2854,18 +2452,9 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] -speedcopy = [ - {file = "speedcopy-2.1.4-py3-none-any.whl", hash = "sha256:e09eb1de67ae0e0b51d5b99a28882009d565a37a3cb3c6bae121e3a5d3cccb17"}, - {file = "speedcopy-2.1.4.tar.gz", hash = "sha256:eff007a97e49ec1934df4fa8074f4bd1cf4a3b14c5499d914988785cff0c199a"}, -] -sphinx = [ - {file = "Sphinx-5.0.1-py3-none-any.whl", hash = "sha256:36aa2a3c2f6d5230be94585bc5d74badd5f9ed8f3388b8eedc1726fe45b1ad30"}, - {file = "Sphinx-5.0.1.tar.gz", hash = "sha256:f4da1187785a5bc7312cc271b0e867a93946c319d106363e102936a3d9857306"}, -] -sphinx-qt-documentation = [ - {file = "sphinx_qt_documentation-0.4-py3-none-any.whl", hash = "sha256:fa131093f75cd1bd48699cd132e18e4d46ba9eaadc070e6026867cea75ecdb7b"}, - {file = "sphinx_qt_documentation-0.4.tar.gz", hash = "sha256:f43ba17baa93e353fb94045027fb67f9d935ed158ce8662de93f08b88eec6774"}, -] +speedcopy = [] +sphinx = [] +sphinx-qt-documentation = [] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, @@ -2914,44 +2503,13 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, -] +typed-ast = [] +typing-extensions = [] uritemplate = [ {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"}, ] -urllib3 = [ - {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, - {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, -] +urllib3 = [] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, @@ -2960,151 +2518,10 @@ websocket-client = [ {file = "websocket-client-0.59.0.tar.gz", hash = "sha256:d376bd60eace9d437ab6d7ee16f4ab4e821c9dae591e1b783c58ebd8aaf80c5c"}, {file = "websocket_client-0.59.0-py2.py3-none-any.whl", hash = "sha256:2e50d26ca593f70aba7b13a489435ef88b8fc3b5c5643c1ce8808ff9b40f0b32"}, ] -wrapt = [ - {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"}, -] +wrapt = [] wsrpc-aiohttp = [ {file = "wsrpc-aiohttp-3.2.0.tar.gz", hash = "sha256:f467abc51bcdc760fc5aeb7041abdeef46eeca3928dc43dd6e7fa7a533563818"}, {file = "wsrpc_aiohttp-3.2.0-py3-none-any.whl", hash = "sha256:fa9b0bf5cb056898cb5c9f64cbc5eacb8a5dd18ab1b7f0cd4a2208b4a7fde282"}, ] -yarl = [ - {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, - {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, - {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, - {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, - {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, - {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, - {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, - {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, - {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, - {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, - {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, - {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, - {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, - {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, - {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, - {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, -] -zipp = [ - {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, - {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, -] +yarl = [] +zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 994c83d369..1d757deaa0 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" +OpenColorIO-Configs = { git = "https://github.com/pypeclub/OpenColorIO-Configs.git", branch = "main" } [tool.poetry.dev-dependencies] @@ -80,13 +81,14 @@ cx_freeze = "~6.9" GitPython = "^3.1.17" jedi = "^0.13" Jinja2 = "^2.11" +markupsafe = "2.0.1" pycodestyle = "^2.5.0" pydocstyle = "^3.0.0" pylint = "^2.4.4" pytest = "^6.1" pytest-cov = "*" pytest-print = "*" -Sphinx = "*" +Sphinx = "5.0.1" sphinx-rtd-theme = "*" sphinxcontrib-websupport = "*" sphinx-qt-documentation = "*" From 8ba5d0079952731a2e7a98490701750bffa28a9e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 18:59:43 +0200 Subject: [PATCH 092/155] move env setup function used in prelaunch hook from api higher --- openpype/hosts/resolve/utils.py | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 openpype/hosts/resolve/utils.py diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py new file mode 100644 index 0000000000..382a7cf344 --- /dev/null +++ b/openpype/hosts/resolve/utils.py @@ -0,0 +1,54 @@ +import os +import shutil +from openpype.lib import Logger + +RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def setup(env): + log = Logger.get_logger("ResolveSetup") + scripts = {} + us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") + us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") + us_paths = [os.path.join( + RESOLVE_ROOT_DIR, + "utility_scripts" + )] + + # collect script dirs + if us_env: + log.info(f"Utility Scripts Env: `{us_env}`") + us_paths = us_env.split( + os.pathsep) + us_paths + + # collect scripts from dirs + for path in us_paths: + scripts.update({path: os.listdir(path)}) + + log.info(f"Utility Scripts Dir: `{us_paths}`") + log.info(f"Utility Scripts: `{scripts}`") + + # make sure no script file is in folder + for s in os.listdir(us_dir): + path = os.path.join(us_dir, s) + log.info(f"Removing `{path}`...") + if os.path.isdir(path): + shutil.rmtree(path, onerror=None) + else: + os.remove(path) + + # copy scripts into Resolve's utility scripts dir + for d, sl in scripts.items(): + # directory and scripts list + for s in sl: + # script in script list + src = os.path.join(d, s) + dst = os.path.join(us_dir, s) + log.info(f"Copying `{src}` to `{dst}`...") + if os.path.isdir(src): + shutil.copytree( + src, dst, symlinks=False, + ignore=None, ignore_dangling_symlinks=False + ) + else: + shutil.copy2(src, dst) From 07d89fc23b593890d10f7094af2be04399f8dedd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 19:01:24 +0200 Subject: [PATCH 093/155] fixed imports to use in-DCC imports from resolve.api --- openpype/hosts/resolve/__init__.py | 129 ----------------- openpype/hosts/resolve/api/__init__.py | 134 +++++++++++++++++- openpype/hosts/resolve/api/action.py | 2 +- openpype/hosts/resolve/api/lib.py | 6 +- openpype/hosts/resolve/api/menu.py | 4 +- openpype/hosts/resolve/api/pipeline.py | 13 +- openpype/hosts/resolve/api/preload_console.py | 4 +- openpype/hosts/resolve/api/utils.py | 96 ++----------- openpype/hosts/resolve/api/workio.py | 19 +-- .../hosts/resolve/hooks/pre_resolve_setup.py | 20 +-- .../plugins/create/create_shot_clip.py | 15 +- .../hosts/resolve/plugins/load/load_clip.py | 21 +-- .../plugins/publish/extract_workfile.py | 4 +- .../plugins/publish/precollect_instances.py | 26 ++-- .../OpenPype_sync_util_scripts.py | 5 +- .../utility_scripts/__OpenPype__Menu__.py | 6 +- .../utility_scripts/tests/test_otio_as_edl.py | 4 +- .../testing_create_timeline_item_from_path.py | 15 +- .../tests/testing_load_media_pool_item.py | 10 +- 19 files changed, 233 insertions(+), 300 deletions(-) diff --git a/openpype/hosts/resolve/__init__.py b/openpype/hosts/resolve/__init__.py index 3e49ce3b9b..e69de29bb2 100644 --- a/openpype/hosts/resolve/__init__.py +++ b/openpype/hosts/resolve/__init__.py @@ -1,129 +0,0 @@ -from .api.utils import ( - setup, - get_resolve_module -) - -from .api.pipeline import ( - install, - uninstall, - ls, - containerise, - update_container, - publish, - launch_workfiles_app, - maintained_selection, - remove_instance, - list_instances -) - -from .api.lib import ( - maintain_current_timeline, - publish_clip_color, - get_project_manager, - get_current_project, - get_current_timeline, - create_bin, - get_media_pool_item, - create_media_pool_item, - create_timeline_item, - get_timeline_item, - get_video_track_names, - get_current_timeline_items, - get_pype_timeline_item_by_name, - get_timeline_item_pype_tag, - set_timeline_item_pype_tag, - imprint, - set_publish_attribute, - get_publish_attribute, - create_compound_clip, - swap_clips, - get_pype_clip_metadata, - set_project_manager_to_folder_name, - get_otio_clip_instance_data, - get_reformated_path -) - -from .api.menu import launch_pype_menu - -from .api.plugin import ( - ClipLoader, - TimelineItemLoader, - Creator, - PublishClip -) - -from .api.workio import ( - open_file, - save_file, - current_file, - has_unsaved_changes, - file_extensions, - work_root -) - -from .api.testing_utils import TestGUI - - -__all__ = [ - # pipeline - "install", - "uninstall", - "ls", - "containerise", - "update_container", - "reload_pipeline", - "publish", - "launch_workfiles_app", - "maintained_selection", - "remove_instance", - "list_instances", - - # utils - "setup", - "get_resolve_module", - - # lib - "maintain_current_timeline", - "publish_clip_color", - "get_project_manager", - "get_current_project", - "get_current_timeline", - "create_bin", - "get_media_pool_item", - "create_media_pool_item", - "create_timeline_item", - "get_timeline_item", - "get_video_track_names", - "get_current_timeline_items", - "get_pype_timeline_item_by_name", - "get_timeline_item_pype_tag", - "set_timeline_item_pype_tag", - "imprint", - "set_publish_attribute", - "get_publish_attribute", - "create_compound_clip", - "swap_clips", - "get_pype_clip_metadata", - "set_project_manager_to_folder_name", - "get_otio_clip_instance_data", - "get_reformated_path", - - # menu - "launch_pype_menu", - - # plugin - "ClipLoader", - "TimelineItemLoader", - "Creator", - "PublishClip", - - # workio - "open_file", - "save_file", - "current_file", - "has_unsaved_changes", - "file_extensions", - "work_root", - - "TestGUI" -] diff --git a/openpype/hosts/resolve/api/__init__.py b/openpype/hosts/resolve/api/__init__.py index 48bd938e57..cf1edb4c35 100644 --- a/openpype/hosts/resolve/api/__init__.py +++ b/openpype/hosts/resolve/api/__init__.py @@ -1,11 +1,137 @@ """ resolve api """ -import os bmdvr = None bmdvf = None -API_DIR = os.path.dirname(os.path.abspath(__file__)) -HOST_DIR = os.path.dirname(API_DIR) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +from .utils import ( + get_resolve_module +) + +from .pipeline import ( + install, + uninstall, + ls, + containerise, + update_container, + publish, + launch_workfiles_app, + maintained_selection, + remove_instance, + list_instances +) + +from .lib import ( + maintain_current_timeline, + publish_clip_color, + get_project_manager, + get_current_project, + get_current_timeline, + create_bin, + get_media_pool_item, + create_media_pool_item, + create_timeline_item, + get_timeline_item, + get_video_track_names, + get_current_timeline_items, + get_pype_timeline_item_by_name, + get_timeline_item_pype_tag, + set_timeline_item_pype_tag, + imprint, + set_publish_attribute, + get_publish_attribute, + create_compound_clip, + swap_clips, + get_pype_clip_metadata, + set_project_manager_to_folder_name, + get_otio_clip_instance_data, + get_reformated_path +) + +from .menu import launch_pype_menu + +from .plugin import ( + ClipLoader, + TimelineItemLoader, + Creator, + PublishClip +) + +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root +) + +from .testing_utils import TestGUI + + +__all__ = [ + "bmdvr", + "bmdvf", + + # pipeline + "install", + "uninstall", + "ls", + "containerise", + "update_container", + "reload_pipeline", + "publish", + "launch_workfiles_app", + "maintained_selection", + "remove_instance", + "list_instances", + + # utils + "get_resolve_module", + + # lib + "maintain_current_timeline", + "publish_clip_color", + "get_project_manager", + "get_current_project", + "get_current_timeline", + "create_bin", + "get_media_pool_item", + "create_media_pool_item", + "create_timeline_item", + "get_timeline_item", + "get_video_track_names", + "get_current_timeline_items", + "get_pype_timeline_item_by_name", + "get_timeline_item_pype_tag", + "set_timeline_item_pype_tag", + "imprint", + "set_publish_attribute", + "get_publish_attribute", + "create_compound_clip", + "swap_clips", + "get_pype_clip_metadata", + "set_project_manager_to_folder_name", + "get_otio_clip_instance_data", + "get_reformated_path", + + # menu + "launch_pype_menu", + + # plugin + "ClipLoader", + "TimelineItemLoader", + "Creator", + "PublishClip", + + # workio + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root", + + "TestGUI" +] diff --git a/openpype/hosts/resolve/api/action.py b/openpype/hosts/resolve/api/action.py index f8f338a850..d55a24a39a 100644 --- a/openpype/hosts/resolve/api/action.py +++ b/openpype/hosts/resolve/api/action.py @@ -4,7 +4,7 @@ from __future__ import absolute_import import pyblish.api -from ...action import get_errored_instances_from_context +from openpype.action import get_errored_instances_from_context class SelectInvalidAction(pyblish.api.Action): diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 93ccdaf812..f41eb36caf 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -4,13 +4,13 @@ import re import os import contextlib from opentimelineio import opentime + +from openpype.lib import Logger from openpype.pipeline.editorial import is_overlapping_otio_ranges from ..otio import davinci_export as otio_export -from openpype.api import Logger - -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) self = sys.modules[__name__] self.project_manager = None diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index 9e0dd12376..2c7678ee5b 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -3,13 +3,13 @@ import sys from Qt import QtWidgets, QtCore +from openpype.tools.utils import host_tools + from .pipeline import ( publish, launch_workfiles_app ) -from openpype.tools.utils import host_tools - def load_stylesheet(): path = os.path.join(os.path.dirname(__file__), "menu_style.qss") diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 4a7d1c5bea..1c8d9dc01c 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -7,7 +7,7 @@ from collections import OrderedDict from pyblish import api as pyblish -from openpype.api import Logger +from openpype.lib import Logger from openpype.pipeline import ( schema, register_loader_plugin_path, @@ -16,11 +16,15 @@ from openpype.pipeline import ( deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) -from . import lib -from . import PLUGINS_DIR from openpype.tools.utils import host_tools -log = Logger().get_logger(__name__) +from . import lib +from .utils import get_resolve_module + +log = Logger.get_logger(__name__) + +HOST_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) +PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") @@ -39,7 +43,6 @@ def install(): See the Maya equivalent for inspiration on how to implement this. """ - from .. import get_resolve_module log.info("openpype.hosts.resolve installed") diff --git a/openpype/hosts/resolve/api/preload_console.py b/openpype/hosts/resolve/api/preload_console.py index 1e3a56b4dd..a822ea2460 100644 --- a/openpype/hosts/resolve/api/preload_console.py +++ b/openpype/hosts/resolve/api/preload_console.py @@ -1,9 +1,9 @@ #!/usr/bin/env python import time from openpype.hosts.resolve.utils import get_resolve_module -from openpype.api import Logger +from openpype.lib import Logger -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) wait_delay = 2.5 wait = 0.00 diff --git a/openpype/hosts/resolve/api/utils.py b/openpype/hosts/resolve/api/utils.py index 9b3762f328..871b3af38d 100644 --- a/openpype/hosts/resolve/api/utils.py +++ b/openpype/hosts/resolve/api/utils.py @@ -4,21 +4,21 @@ Resolve's tools for setting environment """ -import sys import os -import shutil -from . import HOST_DIR -from openpype.api import Logger -log = Logger().get_logger(__name__) +import sys + +from openpype.lib import Logger + +log = Logger.get_logger(__name__) def get_resolve_module(): - from openpype.hosts import resolve + from openpype.hosts.resolve import api # dont run if already loaded - if resolve.api.bmdvr: + if api.bmdvr: log.info(("resolve module is assigned to " - f"`pype.hosts.resolve.api.bmdvr`: {resolve.api.bmdvr}")) - return resolve.api.bmdvr + f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) + return api.bmdvr try: """ The PYTHONPATH needs to be set correctly for this import @@ -71,79 +71,9 @@ def get_resolve_module(): # assign global var and return bmdvr = bmd.scriptapp("Resolve") bmdvf = bmd.scriptapp("Fusion") - resolve.api.bmdvr = bmdvr - resolve.api.bmdvf = bmdvf + api.bmdvr = bmdvr + api.bmdvf = bmdvf log.info(("Assigning resolve module to " - f"`pype.hosts.resolve.api.bmdvr`: {resolve.api.bmdvr}")) + f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) log.info(("Assigning resolve module to " - f"`pype.hosts.resolve.api.bmdvf`: {resolve.api.bmdvf}")) - - -def _sync_utility_scripts(env=None): - """ Synchronizing basic utlility scripts for resolve. - - To be able to run scripts from inside `Resolve/Workspace/Scripts` menu - all scripts has to be accessible from defined folder. - """ - if not env: - env = os.environ - - # initiate inputs - scripts = {} - us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") - us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") - us_paths = [os.path.join( - HOST_DIR, - "utility_scripts" - )] - - # collect script dirs - if us_env: - log.info(f"Utility Scripts Env: `{us_env}`") - us_paths = us_env.split( - os.pathsep) + us_paths - - # collect scripts from dirs - for path in us_paths: - scripts.update({path: os.listdir(path)}) - - log.info(f"Utility Scripts Dir: `{us_paths}`") - log.info(f"Utility Scripts: `{scripts}`") - - # make sure no script file is in folder - if next((s for s in os.listdir(us_dir)), None): - for s in os.listdir(us_dir): - path = os.path.join(us_dir, s) - log.info(f"Removing `{path}`...") - if os.path.isdir(path): - shutil.rmtree(path, onerror=None) - else: - os.remove(path) - - # copy scripts into Resolve's utility scripts dir - for d, sl in scripts.items(): - # directory and scripts list - for s in sl: - # script in script list - src = os.path.join(d, s) - dst = os.path.join(us_dir, s) - log.info(f"Copying `{src}` to `{dst}`...") - if os.path.isdir(src): - shutil.copytree( - src, dst, symlinks=False, - ignore=None, ignore_dangling_symlinks=False - ) - else: - shutil.copy2(src, dst) - - -def setup(env=None): - """ Wrapper installer started from pype.hooks.resolve.ResolvePrelaunch() - """ - if not env: - env = os.environ - - # synchronize resolve utility scripts - _sync_utility_scripts(env) - - log.info("Resolve OpenPype wrapper has been installed") + f"`pype.hosts.resolve.api.bmdvf`: {api.bmdvf}")) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index f175769387..5a742ecf7e 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -2,14 +2,14 @@ import os from openpype.api import Logger -from .. import ( +from .lib import ( get_project_manager, get_current_project, set_project_manager_to_folder_name ) -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) exported_projet_ext = ".drp" @@ -60,7 +60,7 @@ def open_file(filepath): # load project from input path project = pm.LoadProject(fname) log.info(f"Project {project.GetName()} opened...") - return True + except AttributeError: log.warning((f"Project with name `{fname}` does not exist! It will " f"be imported from {filepath} and then loaded...")) @@ -69,9 +69,8 @@ def open_file(filepath): project = pm.LoadProject(fname) log.info(f"Project imported/loaded {project.GetName()}...") return True - else: - return False - + return False + return True def current_file(): pm = get_project_manager() @@ -80,13 +79,9 @@ def current_file(): name = project.GetName() fname = name + exported_projet_ext current_file = os.path.join(current_dir, fname) - normalised = os.path.normpath(current_file) - - # Unsaved current file - if normalised == "": + if not current_file: return None - - return normalised + return os.path.normpath(current_file) def work_root(session): diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 978e3760fd..1d977e2d8e 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 -import importlib + from openpype.lib import PreLaunchHook -from openpype.hosts.resolve.api import utils +from openpype.hosts.resolve.utils import setup class ResolvePrelaunch(PreLaunchHook): @@ -43,18 +43,6 @@ class ResolvePrelaunch(PreLaunchHook): self.launch_context.env.get("PRE_PYTHON_SCRIPT", "")) self.launch_context.env["PRE_PYTHON_SCRIPT"] = pre_py_sc self.log.debug(f"-- pre_py_sc: `{pre_py_sc}`...") - try: - __import__("openpype.hosts.resolve") - __import__("pyblish") - except ImportError: - self.log.warning( - "pyblish: Could not load Resolve integration.", - exc_info=True - ) - - else: - # Resolve Setup integration - importlib.reload(utils) - self.log.debug(f"-- utils.__file__: `{utils.__file__}`") - utils.setup(self.launch_context.env) + # Resolve Setup integration + setup(self.launch_context.env) diff --git a/openpype/hosts/resolve/plugins/create/create_shot_clip.py b/openpype/hosts/resolve/plugins/create/create_shot_clip.py index dbf10c5163..4b14f2493f 100644 --- a/openpype/hosts/resolve/plugins/create/create_shot_clip.py +++ b/openpype/hosts/resolve/plugins/create/create_shot_clip.py @@ -1,9 +1,12 @@ # from pprint import pformat -from openpype.hosts import resolve -from openpype.hosts.resolve.api import lib +from openpype.hosts.resolve.api import plugin, lib +from openpype.hosts.resolve.api.lib import ( + get_video_track_names, + create_bin, +) -class CreateShotClip(resolve.Creator): +class CreateShotClip(plugin.Creator): """Publishable clip""" label = "Create Publishable Clip" @@ -11,7 +14,7 @@ class CreateShotClip(resolve.Creator): icon = "film" defaults = ["Main"] - gui_tracks = resolve.get_video_track_names() + gui_tracks = get_video_track_names() gui_name = "OpenPype publish attributes creator" gui_info = "Define sequential rename and fill hierarchy data." gui_inputs = { @@ -250,7 +253,7 @@ class CreateShotClip(resolve.Creator): sq_markers = self.timeline.GetMarkers() # create media bin for compound clips (trackItems) - mp_folder = resolve.create_bin(self.timeline.GetName()) + mp_folder = create_bin(self.timeline.GetName()) kwargs = { "ui_inputs": widget.result, @@ -264,6 +267,6 @@ class CreateShotClip(resolve.Creator): self.rename_index = i self.log.info(track_item_data) # convert track item to timeline media pool item - track_item = resolve.PublishClip( + track_item = plugin.PublishClip( self, track_item_data, **kwargs).convert() track_item.SetClipColor(lib.publish_clip_color) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 190a5a7206..a0c78c182f 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -1,21 +1,22 @@ from copy import deepcopy -from importlib import reload from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) -from openpype.hosts import resolve +# from openpype.hosts import resolve from openpype.pipeline import ( get_representation_path, legacy_io, ) from openpype.hosts.resolve.api import lib, plugin -reload(plugin) -reload(lib) +from openpype.hosts.resolve.api.pipeline import ( + containerise, + update_container, +) -class LoadClip(resolve.TimelineItemLoader): +class LoadClip(plugin.TimelineItemLoader): """Load a subset to timeline as clip Place clip to timeline on its asset origin timings collected @@ -46,7 +47,7 @@ class LoadClip(resolve.TimelineItemLoader): }) # load clip to timeline and get main variables - timeline_item = resolve.ClipLoader( + timeline_item = plugin.ClipLoader( self, context, **options).load() namespace = namespace or timeline_item.GetName() version = context['version'] @@ -80,7 +81,7 @@ class LoadClip(resolve.TimelineItemLoader): self.log.info("Loader done: `{}`".format(name)) - return resolve.containerise( + return containerise( timeline_item, name, namespace, context, self.__class__.__name__, @@ -98,7 +99,7 @@ class LoadClip(resolve.TimelineItemLoader): context.update({"representation": representation}) name = container['name'] namespace = container['namespace'] - timeline_item_data = resolve.get_pype_timeline_item_by_name(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() version = get_version_by_id(project_name, representation["parent"]) @@ -109,7 +110,7 @@ class LoadClip(resolve.TimelineItemLoader): self.fname = get_representation_path(representation) context["version"] = {"data": version_data} - loader = resolve.ClipLoader(self, context) + loader = plugin.ClipLoader(self, context) timeline_item = loader.update(timeline_item) # add additional metadata from the version to imprint Avalon knob @@ -136,7 +137,7 @@ class LoadClip(resolve.TimelineItemLoader): # update color of clip regarding the version order self.set_item_color(timeline_item, version) - return resolve.update_container(timeline_item, data_imprint) + return update_container(timeline_item, data_imprint) @classmethod def set_item_color(cls, timeline_item, version): diff --git a/openpype/hosts/resolve/plugins/publish/extract_workfile.py b/openpype/hosts/resolve/plugins/publish/extract_workfile.py index e3d60465a2..ea8f19cd8c 100644 --- a/openpype/hosts/resolve/plugins/publish/extract_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/extract_workfile.py @@ -1,7 +1,7 @@ import os import pyblish.api import openpype.api -from openpype.hosts import resolve +from openpype.hosts.resolve.api.lib import get_project_manager class ExtractWorkfile(openpype.api.Extractor): @@ -29,7 +29,7 @@ class ExtractWorkfile(openpype.api.Extractor): os.path.join(staging_dir, drp_file_name)) # write out the drp workfile - resolve.get_project_manager().ExportProject( + get_project_manager().ExportProject( project.GetName(), drp_file_path) # create drp workfile representation diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index ee51998c0d..8ec169ad65 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -1,9 +1,15 @@ -import pyblish -from openpype.hosts import resolve - -# # developer reload modules from pprint import pformat +import pyblish + +from openpype.hosts.resolve.api.lib import ( + get_current_timeline_items, + get_timeline_item_pype_tag, + publish_clip_color, + get_publish_attribute, + get_otio_clip_instance_data, +) + class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" @@ -14,8 +20,8 @@ class PrecollectInstances(pyblish.api.ContextPlugin): def process(self, context): otio_timeline = context.data["otioTimeline"] - selected_timeline_items = resolve.get_current_timeline_items( - filter=True, selecting_color=resolve.publish_clip_color) + selected_timeline_items = get_current_timeline_items( + filter=True, selecting_color=publish_clip_color) self.log.info( "Processing enabled track items: {}".format( @@ -27,7 +33,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): timeline_item = timeline_item_data["clip"]["item"] # get pype tag data - tag_data = resolve.get_timeline_item_pype_tag(timeline_item) + tag_data = get_timeline_item_pype_tag(timeline_item) self.log.debug(f"__ tag_data: {pformat(tag_data)}") if not tag_data: @@ -67,7 +73,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "asset": asset, "item": timeline_item, "families": families, - "publish": resolve.get_publish_attribute(timeline_item), + "publish": get_publish_attribute(timeline_item), "fps": context.data["fps"], "handleStart": handle_start, "handleEnd": handle_end, @@ -75,7 +81,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): }) # otio clip data - otio_data = resolve.get_otio_clip_instance_data( + otio_data = get_otio_clip_instance_data( otio_timeline, timeline_item_data) or {} data.update(otio_data) @@ -134,7 +140,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "asset": asset, "family": family, "families": [], - "publish": resolve.get_publish_attribute(timeline_item) + "publish": get_publish_attribute(timeline_item) }) context.create_instance(**data) diff --git a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py b/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py index 3a16b9c966..8f3917bece 100644 --- a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py +++ b/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py @@ -6,10 +6,11 @@ from openpype.pipeline import install_host def main(env): - import openpype.hosts.resolve as bmdvr + from openpype.hosts.resolve.utils import setup + import openpype.hosts.resolve.api as bmdvr # Registers openpype's Global pyblish plugins install_host(bmdvr) - bmdvr.setup(env) + setup(env) if __name__ == "__main__": diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py index 89ade9238b..1087a7b7a0 100644 --- a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py +++ b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py @@ -2,13 +2,13 @@ import os import sys from openpype.pipeline import install_host -from openpype.api import Logger +from openpype.lib import Logger -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) def main(env): - import openpype.hosts.resolve as bmdvr + import openpype.hosts.resolve.api as bmdvr # activate resolve from openpype install_host(bmdvr) diff --git a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py index 8433bd9172..92f2e43a72 100644 --- a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py +++ b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py @@ -6,8 +6,8 @@ import opentimelineio as otio from openpype.pipeline import install_host -from openpype.hosts.resolve import TestGUI -import openpype.hosts.resolve as bmdvr +import openpype.hosts.resolve.api as bmdvr +from openpype.hosts.resolve.api.testing_utils import TestGUI from openpype.hosts.resolve.otio import davinci_export as otio_export diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py index 477955d527..91a361ec08 100644 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py @@ -2,11 +2,16 @@ import os import sys -from openpype.pipeline import install_host -from openpype.hosts.resolve import TestGUI -import openpype.hosts.resolve as bmdvr import clique +from openpype.pipeline import install_host +from openpype.hosts.resolve.api.testing_utils import TestGUI +import openpype.hosts.resolve.api as bmdvr +from openpype.hosts.resolve.api.lib import ( + create_media_pool_item, + create_timeline_item, +) + class ThisTestGUI(TestGUI): extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] @@ -55,10 +60,10 @@ class ThisTestGUI(TestGUI): # skip if unwanted extension if ext not in self.extensions: return - media_pool_item = bmdvr.create_media_pool_item(fpath) + media_pool_item = create_media_pool_item(fpath) print(media_pool_item) - track_item = bmdvr.create_timeline_item(media_pool_item) + track_item = create_timeline_item(media_pool_item) print(track_item) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py index 872d620162..2e83188bde 100644 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py @@ -1,13 +1,17 @@ #! python3 from openpype.pipeline import install_host -import openpype.hosts.resolve as bmdvr +from openpype.hosts.resolve import api as bmdvr +from openpype.hosts.resolve.api.lib import ( + create_media_pool_item, + create_timeline_item, +) def file_processing(fpath): - media_pool_item = bmdvr.create_media_pool_item(fpath) + media_pool_item = create_media_pool_item(fpath) print(media_pool_item) - track_item = bmdvr.create_timeline_item(media_pool_item) + track_item = create_timeline_item(media_pool_item) print(track_item) From a777238d83282e24c6238b92143b3a424ccde40d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 19:11:49 +0200 Subject: [PATCH 094/155] fix handling of host name in error message --- openpype/host/host.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 48907e7ec7..9cdbb819e1 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -19,8 +19,15 @@ class MissingMethodsError(ValueError): joined_missing = ", ".join( ['"{}"'.format(item) for item in missing_methods] ) + if isinstance(host, HostBase): + host_name = host.name + else: + try: + host_name = host.__file__.replace("\\", "/").split("/")[-3] + except Exception: + host_name = str(host) message = ( - "Host \"{}\" miss methods {}".format(host.name, joined_missing) + "Host \"{}\" miss methods {}".format(host_name, joined_missing) ) super(MissingMethodsError, self).__init__(message) From 312b6d3243ce66bc2e2749c964fd1b178369f9ec Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 13:00:41 +0200 Subject: [PATCH 095/155] :bug: fix finding of last version --- igniter/bootstrap_repos.py | 73 ++++++++++++++++++-------------------- start.py | 3 ++ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 750b2f1bf7..73ef8283a7 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -514,6 +514,9 @@ class OpenPypeVersion(semver.VersionInfo): ValueError: if invalid path is specified. """ + installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version _openpype_versions = [] if not openpype_dir.exists() and not openpype_dir.is_dir(): return _openpype_versions @@ -540,8 +543,7 @@ class OpenPypeVersion(semver.VersionInfo): )[0]: continue - if compatible_with and not detected_version.is_compatible( - compatible_with): + if not detected_version.is_compatible(compatible_with): continue detected_version.path = item @@ -610,6 +612,8 @@ class OpenPypeVersion(semver.VersionInfo): remote = True installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version local_versions = [] remote_versions = [] if local: @@ -630,8 +634,7 @@ class OpenPypeVersion(semver.VersionInfo): all_versions.sort() latest_version: OpenPypeVersion latest_version = all_versions[-1] - if compatible_with and not latest_version.is_compatible( - compatible_with): + if not latest_version.is_compatible(compatible_with): return None return latest_version @@ -1153,10 +1156,12 @@ class BootstrapRepos: versions compatible with specified one. """ + installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version if isinstance(version, str): version = OpenPypeVersion(version=version) - installed_version = OpenPypeVersion.get_installed_version() if installed_version == version: return installed_version @@ -1250,51 +1255,41 @@ class BootstrapRepos: ok install it as normal version. """ + installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version if openpype_path and not isinstance(openpype_path, Path): raise NotImplementedError( ("Finding OpenPype in non-filesystem locations is" " not implemented yet.")) - version_dir = "" - if compatible_with: - version_dir = f"{compatible_with.major}.{compatible_with.minor}" + version_dir = f"{compatible_with.major}.{compatible_with.minor}" # if checks bellow for OPENPYPE_PATH and registry fails, use data_dir # DEPRECATED: lookup in root of this folder is deprecated in favour # of major.minor sub-folders. - dirs_to_search = [ - self.data_dir - ] - if compatible_with: - dirs_to_search.append(self.data_dir / version_dir) + dirs_to_search = [self.data_dir, self.data_dir / version_dir] if openpype_path: - dirs_to_search = [openpype_path] - - if compatible_with: - dirs_to_search.append(openpype_path / version_dir) - else: + dirs_to_search = [openpype_path, openpype_path / version_dir] + elif os.getenv("OPENPYPE_PATH") \ + and Path(os.getenv("OPENPYPE_PATH")).exists(): # first try OPENPYPE_PATH and if that is not available, # try registry. - if os.getenv("OPENPYPE_PATH") \ - and Path(os.getenv("OPENPYPE_PATH")).exists(): - dirs_to_search = [Path(os.getenv("OPENPYPE_PATH"))] + dirs_to_search = [Path(os.getenv("OPENPYPE_PATH")), + Path(os.getenv("OPENPYPE_PATH")) / version_dir] + else: + try: + registry_dir = Path( + str(self.registry.get_item("openPypePath"))) + if registry_dir.exists(): + dirs_to_search = [ + registry_dir, registry_dir / version_dir + ] - if compatible_with: - dirs_to_search.append( - Path(os.getenv("OPENPYPE_PATH")) / version_dir) - else: - try: - registry_dir = Path( - str(self.registry.get_item("openPypePath"))) - if registry_dir.exists(): - dirs_to_search = [registry_dir] - if compatible_with: - dirs_to_search.append(registry_dir / version_dir) - - except ValueError: - # nothing found in registry, we'll use data dir - pass + except ValueError: + # nothing found in registry, we'll use data dir + pass openpype_versions = [] for dir_to_search in dirs_to_search: @@ -1685,6 +1680,9 @@ class BootstrapRepos: ValueError: if invalid path is specified. """ + installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version if not openpype_dir.exists() and not openpype_dir.is_dir(): raise ValueError(f"specified directory {openpype_dir} is invalid") @@ -1711,8 +1709,7 @@ class BootstrapRepos: ): continue - if compatible_with and \ - not detected_version.is_compatible(compatible_with): + if not detected_version.is_compatible(compatible_with): continue detected_version.path = item diff --git a/start.py b/start.py index 5cdffafb6e..c7bced20bd 100644 --- a/start.py +++ b/start.py @@ -629,6 +629,9 @@ def _determine_mongodb() -> str: def _initialize_environment(openpype_version: OpenPypeVersion) -> None: version_path = openpype_version.path + if not version_path: + _print(f"!!! Version {openpype_version} doesn't have path set.") + raise ValueError("No path set in specified OpenPype version.") os.environ["OPENPYPE_VERSION"] = str(openpype_version) # set OPENPYPE_REPOS_ROOT to point to currently used OpenPype version. os.environ["OPENPYPE_REPOS_ROOT"] = os.path.normpath( From 089cd3f9fa3587178c9fe73371b4470588b8467b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 13:55:00 +0200 Subject: [PATCH 096/155] added missing docstring for 'context_filters' argument --- openpype/client/entities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index c798c0ad6d..67ddb09ddb 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1220,6 +1220,8 @@ def get_archived_representations( 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, re.Pattern]]): 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 (Iterable[str]): Fields that should be returned. All fields are From aefb992ce55145f94790bbaa5cdbf17136684e1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 13:55:35 +0200 Subject: [PATCH 097/155] removed unused 'is_context' property --- .../workfile/abstract_template_loader.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 1c8ede25e6..e2f9fdba0f 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -465,23 +465,6 @@ class AbstractPlaceholder: return self.data["loader"] - @property - def is_context(self): - """Check if is placeholder context type. - - context_asset: For loading current asset - linked_asset: For loading linked assets - - Question: - There seems to be more build options and this property is not used, - should be removed? - - Returns: - bool: true if placeholder is a context placeholder - """ - - return self.builder_type == "context_asset" - @property def is_valid(self): """Test validity of placeholder. From 32c2440e4a6d5d5ec3d54c2d1a44ffc5f0f81ae5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 13:55:55 +0200 Subject: [PATCH 098/155] fix docstring header --- openpype/pipeline/workfile/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index e2f9fdba0f..05a98a1ddc 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -456,7 +456,7 @@ class AbstractPlaceholder: @property def loader_name(self): - """Return placeholder loader type. + """Return placeholder loader name. Returns: str: Loader name that will be used to load placeholder From 0c72b8e278d3e0ac2af5b69ff09b115908a4b632 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 15:31:56 +0200 Subject: [PATCH 099/155] :recycle: refactor compatibility check --- igniter/bootstrap_repos.py | 143 +++++++++++++++++++------------------ igniter/install_thread.py | 19 ++++- openpype/version.py | 2 +- start.py | 24 +++---- 4 files changed, 104 insertions(+), 84 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 73ef8283a7..3a2dbe81c4 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -411,16 +411,7 @@ class OpenPypeVersion(semver.VersionInfo): # 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, compatible_with=compatible_with - ) - if compatible_with: - dir_to_search = Path( - user_data_dir("openpype", "pypeclub")) / f"{compatible_with.major}.{compatible_with.minor}" # noqa - versions += OpenPypeVersion.get_versions_from_directory( - dir_to_search, compatible_with=compatible_with - ) - + versions = OpenPypeVersion.get_versions_from_directory(dir_to_search) filtered_versions = [] for version in versions: @@ -498,14 +489,11 @@ class OpenPypeVersion(semver.VersionInfo): @staticmethod def get_versions_from_directory( - openpype_dir: Path, - compatible_with: OpenPypeVersion = None) -> List: + openpype_dir: Path) -> List: """Get all detected OpenPype versions in directory. Args: openpype_dir (Path): Directory to scan. - compatible_with (OpenPypeVersion): Return only versions compatible - with build version specified as OpenPypeVersion. Returns: list of OpenPypeVersion @@ -515,17 +503,27 @@ class OpenPypeVersion(semver.VersionInfo): """ installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version - _openpype_versions = [] + openpype_versions = [] if not openpype_dir.exists() and not openpype_dir.is_dir(): - return _openpype_versions + return openpype_versions # iterate over directory in first level and find all that might # contain OpenPype. for item in openpype_dir.iterdir(): - - # if file, strip extension, in case of dir not. + # if the item is directory with major.minor version, dive deeper + try: + ver_dir = item.name.split(".")[ + 0] == installed_version.major and \ + item.name.split(".")[ + 1] == installed_version.minor # noqa: E051 + if item.is_dir() and ver_dir: + _versions = OpenPypeVersion.get_versions_from_directory( + item) + if _versions: + openpype_versions.append(_versions) + except IndexError: + pass + # if file exists, strip extension, in case of dir don't. name = item.name if item.is_dir() else item.stem result = OpenPypeVersion.version_in_str(name) @@ -543,13 +541,10 @@ class OpenPypeVersion(semver.VersionInfo): )[0]: continue - if not detected_version.is_compatible(compatible_with): - continue - detected_version.path = item - _openpype_versions.append(detected_version) + openpype_versions.append(detected_version) - return sorted(_openpype_versions) + return sorted(openpype_versions) @staticmethod def get_installed_version_str() -> str: @@ -577,15 +572,14 @@ class OpenPypeVersion(semver.VersionInfo): def get_latest_version( staging: bool = False, local: bool = None, - remote: bool = None, - compatible_with: OpenPypeVersion = None + remote: bool = None ) -> Union[OpenPypeVersion, None]: - """Get latest available version. + """Get the latest available version. The version does not contain information about path and source. - This is utility version to get latest version from all found. Build - version is not listed if staging is enabled. + This is utility version to get the latest version from all found. + Build version is not listed if staging is enabled. Arguments 'local' and 'remote' define if local and remote repository versions are used. All versions are used if both are not set (or set @@ -597,8 +591,9 @@ class OpenPypeVersion(semver.VersionInfo): staging (bool, optional): List staging versions if True. local (bool, optional): List local versions if True. remote (bool, optional): List remote versions if True. - compatible_with (OpenPypeVersion, optional) Return only version - compatible with compatible_with. + + Returns: + Latest OpenPypeVersion or None """ if local is None and remote is None: @@ -612,8 +607,6 @@ class OpenPypeVersion(semver.VersionInfo): remote = True installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version local_versions = [] remote_versions = [] if local: @@ -633,10 +626,7 @@ class OpenPypeVersion(semver.VersionInfo): all_versions.sort() latest_version: OpenPypeVersion - latest_version = all_versions[-1] - if not latest_version.is_compatible(compatible_with): - return None - return latest_version + return all_versions[-1] @classmethod def get_expected_studio_version(cls, staging=False, global_settings=None): @@ -1191,13 +1181,27 @@ class BootstrapRepos: @staticmethod def find_latest_openpype_version( - staging, compatible_with: OpenPypeVersion = None): + staging: bool, + compatible_with: OpenPypeVersion = None + ) -> Union[OpenPypeVersion, None]: + """Find the latest available OpenPype version in all location. + + Args: + staging (bool): True to look for staging versions. + compatible_with (OpenPypeVersion, optional): If set, it will + try to find the latest version compatible with the + one specified. + + Returns: + Latest OpenPype version on None if nothing was found. + + """ installed_version = OpenPypeVersion.get_installed_version() local_versions = OpenPypeVersion.get_local_versions( - staging=staging, compatible_with=compatible_with + staging=staging ) remote_versions = OpenPypeVersion.get_remote_versions( - staging=staging, compatible_with=compatible_with + staging=staging ) all_versions = local_versions + remote_versions if not staging: @@ -1206,6 +1210,12 @@ class BootstrapRepos: if not all_versions: return None + if compatible_with: + all_versions = [ + version for version in all_versions + if version.is_compatible(installed_version) + ] + all_versions.sort() latest_version = all_versions[-1] if latest_version == installed_version: @@ -1222,8 +1232,7 @@ class BootstrapRepos: self, openpype_path: Union[Path, str] = None, staging: bool = False, - include_zips: bool = False, - compatible_with: OpenPypeVersion = None + include_zips: bool = False ) -> Union[List[OpenPypeVersion], None]: """Get ordered dict of detected OpenPype version. @@ -1256,36 +1265,29 @@ class BootstrapRepos: """ installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version if openpype_path and not isinstance(openpype_path, Path): raise NotImplementedError( ("Finding OpenPype in non-filesystem locations is" " not implemented yet.")) - version_dir = f"{compatible_with.major}.{compatible_with.minor}" - # if checks bellow for OPENPYPE_PATH and registry fails, use data_dir # DEPRECATED: lookup in root of this folder is deprecated in favour # of major.minor sub-folders. - dirs_to_search = [self.data_dir, self.data_dir / version_dir] + dirs_to_search = [self.data_dir] if openpype_path: - dirs_to_search = [openpype_path, openpype_path / version_dir] + dirs_to_search = [openpype_path] elif os.getenv("OPENPYPE_PATH") \ and Path(os.getenv("OPENPYPE_PATH")).exists(): # first try OPENPYPE_PATH and if that is not available, # try registry. - dirs_to_search = [Path(os.getenv("OPENPYPE_PATH")), - Path(os.getenv("OPENPYPE_PATH")) / version_dir] + dirs_to_search = [Path(os.getenv("OPENPYPE_PATH"))] else: try: registry_dir = Path( str(self.registry.get_item("openPypePath"))) if registry_dir.exists(): - dirs_to_search = [ - registry_dir, registry_dir / version_dir - ] + dirs_to_search = [registry_dir] except ValueError: # nothing found in registry, we'll use data dir @@ -1295,7 +1297,7 @@ class BootstrapRepos: for dir_to_search in dirs_to_search: try: openpype_versions += self.get_openpype_versions( - dir_to_search, staging, compatible_with=compatible_with) + dir_to_search, staging) except ValueError: # location is invalid, skip it pass @@ -1663,15 +1665,12 @@ class BootstrapRepos: def get_openpype_versions( self, openpype_dir: Path, - staging: bool = False, - compatible_with: OpenPypeVersion = None) -> list: + staging: bool = False) -> list: """Get all detected OpenPype versions in directory. Args: openpype_dir (Path): Directory to scan. staging (bool, optional): Find staging versions if True. - compatible_with (OpenPypeVersion, optional): Get only versions - compatible with the one specified. Returns: list of OpenPypeVersion @@ -1681,17 +1680,24 @@ class BootstrapRepos: """ installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version if not openpype_dir.exists() and not openpype_dir.is_dir(): raise ValueError(f"specified directory {openpype_dir} is invalid") - _openpype_versions = [] + openpype_versions = [] # iterate over directory in first level and find all that might # contain OpenPype. for item in openpype_dir.iterdir(): - - # if file, strip extension, in case of dir not. + # if the item is directory with major.minor version, dive deeper + try: + ver_dir = item.name.split(".")[0] == installed_version.major and item.name.split(".")[1] == installed_version.minor # noqa: E051 + if item.is_dir() and ver_dir: + _versions = self.get_openpype_versions( + item, staging=staging) + if _versions: + openpype_versions.append(_versions) + except IndexError: + pass + # if it is file, strip extension, in case of dir don't. name = item.name if item.is_dir() else item.stem result = OpenPypeVersion.version_in_str(name) @@ -1709,17 +1715,14 @@ class BootstrapRepos: ): continue - if not detected_version.is_compatible(compatible_with): - continue - detected_version.path = item if staging and detected_version.is_staging(): - _openpype_versions.append(detected_version) + openpype_versions.append(detected_version) if not staging and not detected_version.is_staging(): - _openpype_versions.append(detected_version) + openpype_versions.append(detected_version) - return sorted(_openpype_versions) + return sorted(openpype_versions) class OpenPypeVersionExists(Exception): diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 8e31f8cb8f..0cccf664e7 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -62,7 +62,7 @@ class InstallThread(QThread): progress_callback=self.set_progress, message=self.message) local_version = OpenPypeVersion.get_installed_version_str() - # if user did entered nothing, we install OpenPype from local version. + # if user did enter nothing, we install OpenPype from local version. # zip content of `repos`, copy it to user data dir and append # version to it. if not self._path: @@ -93,6 +93,23 @@ class InstallThread(QThread): detected = bs.find_openpype(include_zips=True) if detected: + if not OpenPypeVersion.get_installed_version().is_compatible( + detected[-1]): + self.message.emit(( + f"Latest detected version {detected[-1]} " + "is not compatible with the currently running " + f"{local_version}" + ), True) + self.message.emit(( + "Filtering detected versions to compatible ones..." + ), False) + + detected = [ + version for version in detected + if version.is_compatible( + OpenPypeVersion.get_installed_version()) + ] + if OpenPypeVersion( version=local_version, path=Path()) < detected[-1]: self.message.emit(( diff --git a/openpype/version.py b/openpype/version.py index c41e69d00d..d85f9f60ed 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -"""Package declaring Pype version.""" +"""Package declaring OpenPype version.""" __version__ = "3.13.1-nightly.1" diff --git a/start.py b/start.py index c7bced20bd..52e98bb6e1 100644 --- a/start.py +++ b/start.py @@ -699,8 +699,7 @@ def _find_frozen_openpype(use_version: str = None, # Version says to use latest version _print(">>> Finding latest version defined by use version") openpype_version = bootstrap.find_latest_openpype_version( - use_staging, compatible_with=installed_version - ) + use_staging) else: _print(f">>> Finding specified version \"{use_version}\"") openpype_version = bootstrap.find_openpype_version( @@ -712,18 +711,11 @@ def _find_frozen_openpype(use_version: str = None, f"Requested version \"{use_version}\" was not found." ) - if not openpype_version.is_compatible(installed_version): - raise OpenPypeVersionIncompatible(( - f"Requested version \"{use_version}\" is not compatible " - f"with installed version \"{installed_version}\"" - )) - elif studio_version is not None: # Studio has defined a version to use _print(f">>> Finding studio version \"{studio_version}\"") openpype_version = bootstrap.find_openpype_version( - studio_version, use_staging, compatible_with=installed_version - ) + studio_version, use_staging) if openpype_version is None: raise OpenPypeVersionNotFound(( "Requested OpenPype version " @@ -737,8 +729,8 @@ def _find_frozen_openpype(use_version: str = None, ">>> Finding latest version compatible " f"with [ {installed_version} ]")) openpype_version = bootstrap.find_latest_openpype_version( - use_staging, compatible_with=installed_version - ) + use_staging, compatible_with=installed_version) + if openpype_version is None: if use_staging: reason = "Didn't find any staging versions." @@ -756,6 +748,14 @@ def _find_frozen_openpype(use_version: str = None, _initialize_environment(openpype_version) return version_path + if not installed_version.is_compatible(openpype_version): + raise OpenPypeVersionIncompatible( + ( + f"Latest version found {openpype_version} is not " + f"compatible with currently running {installed_version}" + ) + ) + # test if latest detected is installed (in user data dir) is_inside = False try: From c1d3d704106638e1d28ef338a958496790578c40 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 15:40:17 +0200 Subject: [PATCH 100/155] :rotating_light: fix hound :dog: --- igniter/bootstrap_repos.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 3a2dbe81c4..6a04198fc9 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -1264,7 +1264,6 @@ class BootstrapRepos: ok install it as normal version. """ - installed_version = OpenPypeVersion.get_installed_version() if openpype_path and not isinstance(openpype_path, Path): raise NotImplementedError( ("Finding OpenPype in non-filesystem locations is" @@ -1689,7 +1688,7 @@ class BootstrapRepos: for item in openpype_dir.iterdir(): # if the item is directory with major.minor version, dive deeper try: - ver_dir = item.name.split(".")[0] == installed_version.major and item.name.split(".")[1] == installed_version.minor # noqa: E051 + ver_dir = item.name.split(".")[0] == installed_version.major and item.name.split(".")[1] == installed_version.minor # noqa: E501 if item.is_dir() and ver_dir: _versions = self.get_openpype_versions( item, staging=staging) From 7176723aa5f8710ca422d9fd40577a6b85bc7b81 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 17:26:46 +0200 Subject: [PATCH 101/155] :bug: fix arguments and recursive folders --- igniter/bootstrap_repos.py | 44 +++++++++++++------------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 6a04198fc9..01d7c4bb7e 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -425,7 +425,7 @@ class OpenPypeVersion(semver.VersionInfo): @classmethod def get_remote_versions( cls, production: bool = None, - staging: bool = None, compatible_with: OpenPypeVersion = None + staging: bool = None ) -> List: """Get all versions available in OpenPype Path. @@ -470,13 +470,7 @@ class OpenPypeVersion(semver.VersionInfo): if not dir_to_search: return [] - # DEPRECATED: look for version in root directory - versions = cls.get_versions_from_directory( - dir_to_search, compatible_with=compatible_with) - if compatible_with: - dir_to_search = dir_to_search / f"{compatible_with.major}.{compatible_with.minor}" # noqa - versions += cls.get_versions_from_directory( - dir_to_search, compatible_with=compatible_with) + versions = cls.get_versions_from_directory(dir_to_search) filtered_versions = [] for version in versions: @@ -511,18 +505,13 @@ class OpenPypeVersion(semver.VersionInfo): # contain OpenPype. for item in openpype_dir.iterdir(): # if the item is directory with major.minor version, dive deeper - try: - ver_dir = item.name.split(".")[ - 0] == installed_version.major and \ - item.name.split(".")[ - 1] == installed_version.minor # noqa: E051 - if item.is_dir() and ver_dir: - _versions = OpenPypeVersion.get_versions_from_directory( - item) - if _versions: - openpype_versions.append(_versions) - except IndexError: - pass + + if item.is_dir() and re.match(r"^\d+\.\d+$", item.name): + _versions = OpenPypeVersion.get_versions_from_directory( + item) + if _versions: + openpype_versions += _versions + # if file exists, strip extension, in case of dir don't. name = item.name if item.is_dir() else item.stem result = OpenPypeVersion.version_in_str(name) @@ -1687,15 +1676,12 @@ class BootstrapRepos: # contain OpenPype. for item in openpype_dir.iterdir(): # if the item is directory with major.minor version, dive deeper - try: - ver_dir = item.name.split(".")[0] == installed_version.major and item.name.split(".")[1] == installed_version.minor # noqa: E501 - if item.is_dir() and ver_dir: - _versions = self.get_openpype_versions( - item, staging=staging) - if _versions: - openpype_versions.append(_versions) - except IndexError: - pass + if item.is_dir() and re.match(r"^\d+\.\d+$", item.name): + _versions = self.get_openpype_versions( + item, staging=staging) + if _versions: + openpype_versions += _versions + # if it is file, strip extension, in case of dir don't. name = item.name if item.is_dir() else item.stem result = OpenPypeVersion.version_in_str(name) From 60ea9728f63afa2c0ec2c32bd619fec8e64993ec Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 17:43:02 +0200 Subject: [PATCH 102/155] :rotating_light: fix hound :dog: --- igniter/bootstrap_repos.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 01d7c4bb7e..3dab67ebf1 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -496,7 +496,6 @@ class OpenPypeVersion(semver.VersionInfo): ValueError: if invalid path is specified. """ - installed_version = OpenPypeVersion.get_installed_version() openpype_versions = [] if not openpype_dir.exists() and not openpype_dir.is_dir(): return openpype_versions @@ -1667,7 +1666,6 @@ class BootstrapRepos: ValueError: if invalid path is specified. """ - installed_version = OpenPypeVersion.get_installed_version() if not openpype_dir.exists() and not openpype_dir.is_dir(): raise ValueError(f"specified directory {openpype_dir} is invalid") From 8b94d746e5595caa42bafae6184663683fd8e4f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 18:00:26 +0200 Subject: [PATCH 103/155] show outdated build dialog when expected version can't be used with current build --- openpype/tools/tray/pype_tray.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 4e5db06a92..2f3e1bcab3 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -10,19 +10,19 @@ from Qt import QtCore, QtGui, QtWidgets import openpype.version from openpype.api import ( - Logger, resources, get_system_settings ) -from openpype.lib import ( - get_openpype_execute_args, +from openpype.lib import get_openpype_execute_args, Logger +from openpype.lib.openpype_version import ( op_version_control_available, + get_expected_version, + get_installed_version, is_current_version_studio_latest, is_current_version_higher_than_expected, is_running_from_build, is_running_staging, - get_expected_version, - get_openpype_version + get_openpype_version, ) from openpype.modules import TrayModulesManager from openpype import style @@ -329,6 +329,21 @@ class TrayManager: self._version_dialog.close() return + installed_version = get_installed_version() + expected_version = get_expected_version() + + # Request new build if is needed + if not expected_version.is_compatible(installed_version): + if ( + self._version_dialog is not None + and self._version_dialog.isVisible() + ): + self._version_dialog.close() + + dialog = BuildVersionDialog() + dialog.exec_() + return + if self._version_dialog is None: self._version_dialog = VersionUpdateDialog() self._version_dialog.restart_requested.connect( @@ -338,7 +353,6 @@ class TrayManager: self._outdated_version_ignored ) - expected_version = get_expected_version() current_version = get_openpype_version() current_is_higher = is_current_version_higher_than_expected() From ad64c3a66e10c2c34ecd4fe3549f636ce5777959 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 18:02:08 +0200 Subject: [PATCH 104/155] added backwards compatibility for 'is_compatible' method --- openpype/tools/tray/pype_tray.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 2f3e1bcab3..85bc00ead6 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -333,7 +333,11 @@ class TrayManager: expected_version = get_expected_version() # Request new build if is needed - if not expected_version.is_compatible(installed_version): + if ( + # Backwards compatibility + not hasattr(expected_version, "is_compatible") + or not expected_version.is_compatible(installed_version) + ): if ( self._version_dialog is not None and self._version_dialog.isVisible() From 79a3777d8663ca58d855469c60299486374a3539 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 18:41:55 +0200 Subject: [PATCH 105/155] :recycle: distribute ocio as zips --- .../maya/plugins/publish/extract_look.py | 6 ++++- poetry.lock | 25 +++---------------- pyproject.toml | 6 +++-- tools/fetch_thirdparty_libs.py | 23 +++++++++++++---- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b425efba6f..b416669b87 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -43,7 +43,11 @@ def get_ocio_config_path(profile_folder): try: import OpenColorIOConfigs return os.path.join( - os.path.dirname(OpenColorIOConfigs.__file__), + os.environ["OPENPYPE_ROOT"], + "vendor", + "bin", + "ocioconfig" + "OpenColorIOConfigs", profile_folder, "config.ocio" ) diff --git a/poetry.lock b/poetry.lock index df8d8ab14a..21b6bda880 100644 --- a/poetry.lock +++ b/poetry.lock @@ -797,21 +797,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "opencolorio-configs" -version = "1.0.2" -description = "Curated set of OpenColorIO Configs for use in OpenPype" -category = "main" -optional = false -python-versions = "*" -develop = false - -[package.source] -type = "git" -url = "https://github.com/pypeclub/OpenColorIO-Configs.git" -reference = "main" -resolved_reference = "07c5e865bf2b115b589dd2876ae632cd410821b5" - [[package]] name = "opentimelineio" version = "0.14.0.dev1" @@ -1284,7 +1269,7 @@ python-versions = "*" [[package]] name = "pytz" -version = "2022.1" +version = "2022.2" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -1750,7 +1735,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "89fb7e8ad310b5048bf78561f1146194c8779e286d839cc000f04e88be87f3f3" +content-hash = "de7422afb6aed02f75e1696afdda9ad6c7bf32da76b5022ee3e8f71a1ac4bae2" [metadata.files] acre = [] @@ -2146,7 +2131,6 @@ multidict = [ {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, ] -opencolorio-configs = [] opentimelineio = [] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, @@ -2398,10 +2382,7 @@ python-xlib = [ python3-xlib = [ {file = "python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8"}, ] -pytz = [ - {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, - {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, -] +pytz = [] pywin32 = [ {file = "pywin32-301-cp35-cp35m-win32.whl", hash = "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7"}, {file = "pywin32-301-cp35-cp35m-win_amd64.whl", hash = "sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72"}, diff --git a/pyproject.toml b/pyproject.toml index 1d757deaa0..b7b3fb967f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,8 +70,6 @@ requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" -OpenColorIO-Configs = { git = "https://github.com/pypeclub/OpenColorIO-Configs.git", branch = "main" } - [tool.poetry.dev-dependencies] flake8 = "^3.7" @@ -144,6 +142,10 @@ hash = "3894dec7e4e521463891a869586850e8605f5fd604858b674c87323bf33e273d" url = "https://distribute.openpype.io/thirdparty/oiio-2.2.0-darwin.tgz" hash = "sha256:..." +[openpype.thirdparty.ocioconfig] +url = "https://distribute.openpype.io/thirdparty/OpenColorIO-Configs-1.0.2.zip" +hash = "4ac17c1f7de83465e6f51dd352d7117e07e765b66d00443257916c828e35b6ce" + [tool.pyright] include = [ "igniter", diff --git a/tools/fetch_thirdparty_libs.py b/tools/fetch_thirdparty_libs.py index b616beab27..421cc32dbd 100644 --- a/tools/fetch_thirdparty_libs.py +++ b/tools/fetch_thirdparty_libs.py @@ -109,13 +109,20 @@ except AttributeError: for k, v in thirdparty.items(): _print(f"processing {k}") - destination_path = openpype_root / "vendor" / "bin" / k / platform_name - url = v.get(platform_name).get("url") + destination_path = openpype_root / "vendor" / "bin" / k + if not v.get(platform_name): _print(("missing definition for current " - f"platform [ {platform_name} ]"), 1) - sys.exit(1) + f"platform [ {platform_name} ]"), 2) + _print("trying to get universal url for all platforms") + url = v.get("url") + if not url: + _print("cannot get url", 1) + sys.exit(1) + else: + url = v.get(platform_name).get("url") + destination_path = destination_path / platform_name parsed_url = urlparse(url) @@ -147,7 +154,13 @@ for k, v in thirdparty.items(): # get file with checksum _print("Calculating sha256 ...", 2) calc_checksum = sha256_sum(temp_file) - if v.get(platform_name).get("hash") != calc_checksum: + + if v.get(platform_name): + item_hash = v.get(platform_name).get("hash") + else: + item_hash = v.get("hash") + + if item_hash != calc_checksum: _print("Downloaded files checksum invalid.") sys.exit(1) From 5a2e8f6d8f814bd0d7f6707580edd08e0098fec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 12 Aug 2022 18:44:16 +0200 Subject: [PATCH 106/155] :bug: remove import --- .../maya/plugins/publish/extract_look.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b416669b87..cece8ee22b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -40,19 +40,17 @@ def get_ocio_config_path(profile_folder): Returns: str: Path to vendorized config file. """ - try: - import OpenColorIOConfigs - return os.path.join( - os.environ["OPENPYPE_ROOT"], - "vendor", - "bin", - "ocioconfig" - "OpenColorIOConfigs", - profile_folder, - "config.ocio" - ) - except ImportError: - return None + + return os.path.join( + os.environ["OPENPYPE_ROOT"], + "vendor", + "bin", + "ocioconfig" + "OpenColorIOConfigs", + profile_folder, + "config.ocio" + ) + def find_paths_by_hash(texture_hash): """Find the texture hash key in the dictionary. From 1ad9728962b92e55fa4d16601a7a48add381a456 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 19:32:13 +0200 Subject: [PATCH 107/155] :recycle: remove forgotten args, fix typos --- igniter/bootstrap_repos.py | 35 +++++++++++++++-------------------- start.py | 2 +- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 3dab67ebf1..56ec2749ca 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -381,7 +381,7 @@ class OpenPypeVersion(semver.VersionInfo): @classmethod def get_local_versions( cls, production: bool = None, - staging: bool = None, compatible_with: OpenPypeVersion = None + staging: bool = None ) -> List: """Get all versions available on this machine. @@ -391,8 +391,10 @@ class OpenPypeVersion(semver.VersionInfo): Args: production (bool): Return production versions. staging (bool): Return staging versions. - compatible_with (OpenPypeVersion): Return only those compatible - with specified version. + + Returns: + list: of compatible versions available on the machine. + """ # Return all local versions if arguments are set to None if production is None and staging is None: @@ -435,8 +437,7 @@ class OpenPypeVersion(semver.VersionInfo): Args: production (bool): Return production versions. staging (bool): Return staging versions. - compatible_with (OpenPypeVersion): Return only those compatible - with specified version. + """ # Return all local versions if arguments are set to None if production is None and staging is None: @@ -745,9 +746,9 @@ class BootstrapRepos: self, repo_dir: Path = None) -> Union[OpenPypeVersion, None]: """Copy zip created from OpenPype repositories to user data dir. - This detect OpenPype version either in local "live" OpenPype + This detects OpenPype version either in local "live" OpenPype repository or in user provided path. Then it will zip it in temporary - directory and finally it will move it to destination which is user + directory, and finally it will move it to destination which is user data directory. Existing files will be replaced. Args: @@ -758,7 +759,7 @@ class BootstrapRepos: """ # if repo dir is not set, we detect local "live" OpenPype repository - # version and use it as a source. Otherwise repo_dir is user + # version and use it as a source. Otherwise, repo_dir is user # entered location. if repo_dir: version = self.get_version(repo_dir) @@ -1122,21 +1123,19 @@ class BootstrapRepos: @staticmethod def find_openpype_version( version: Union[str, OpenPypeVersion], - staging: bool, - compatible_with: OpenPypeVersion = None + staging: bool ) -> Union[OpenPypeVersion, None]: """Find location of specified OpenPype version. Args: version (Union[str, OpenPypeVersion): Version to find. staging (bool): Filter staging versions. - compatible_with (OpenPypeVersion, optional): Find only - versions compatible with specified one. + + Returns: + requested OpenPypeVersion. """ installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version if isinstance(version, str): version = OpenPypeVersion(version=version) @@ -1144,8 +1143,7 @@ class BootstrapRepos: return installed_version local_versions = OpenPypeVersion.get_local_versions( - staging=staging, production=not staging, - compatible_with=compatible_with + staging=staging, production=not staging ) zip_version = None for local_version in local_versions: @@ -1159,8 +1157,7 @@ class BootstrapRepos: return zip_version remote_versions = OpenPypeVersion.get_remote_versions( - staging=staging, production=not staging, - compatible_with=compatible_with + staging=staging, production=not staging ) for remote_version in remote_versions: if remote_version == version: @@ -1237,8 +1234,6 @@ class BootstrapRepos: otherwise. include_zips (bool, optional): If set True it will try to find OpenPype in zip files in given directory. - compatible_with (OpenPypeVersion, optional): Find only those - versions compatible with the one specified. Returns: dict of Path: Dictionary of detected OpenPype version. diff --git a/start.py b/start.py index 52e98bb6e1..bfbcc77bc9 100644 --- a/start.py +++ b/start.py @@ -689,7 +689,7 @@ def _find_frozen_openpype(use_version: str = None, # Collect OpenPype versions installed_version = OpenPypeVersion.get_installed_version() # Expected version that should be used by studio settings - # - this option is used only if version is not explictly set and if + # - this option is used only if version is not explicitly set and if # studio has set explicit version in settings studio_version = OpenPypeVersion.get_expected_studio_version(use_staging) From b61e47a15d4ea7f843aa5a17963f8f4d0d73c77f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 19:45:26 +0200 Subject: [PATCH 108/155] :recycle: don't look for compatible version automatically --- igniter/bootstrap_repos.py | 12 +----------- start.py | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 56ec2749ca..dfcca2cf33 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -1166,16 +1166,12 @@ class BootstrapRepos: @staticmethod def find_latest_openpype_version( - staging: bool, - compatible_with: OpenPypeVersion = None + staging: bool ) -> Union[OpenPypeVersion, None]: """Find the latest available OpenPype version in all location. Args: staging (bool): True to look for staging versions. - compatible_with (OpenPypeVersion, optional): If set, it will - try to find the latest version compatible with the - one specified. Returns: Latest OpenPype version on None if nothing was found. @@ -1195,12 +1191,6 @@ class BootstrapRepos: if not all_versions: return None - if compatible_with: - all_versions = [ - version for version in all_versions - if version.is_compatible(installed_version) - ] - all_versions.sort() latest_version = all_versions[-1] if latest_version == installed_version: diff --git a/start.py b/start.py index bfbcc77bc9..9837252a1f 100644 --- a/start.py +++ b/start.py @@ -729,7 +729,7 @@ def _find_frozen_openpype(use_version: str = None, ">>> Finding latest version compatible " f"with [ {installed_version} ]")) openpype_version = bootstrap.find_latest_openpype_version( - use_staging, compatible_with=installed_version) + use_staging) if openpype_version is None: if use_staging: From aa0fe93a504a3a513239c541e698a99600de9736 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 19:50:07 +0200 Subject: [PATCH 109/155] :bug: fix version list --- start.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/start.py b/start.py index 9837252a1f..084eb7451a 100644 --- a/start.py +++ b/start.py @@ -726,7 +726,7 @@ def _find_frozen_openpype(use_version: str = None, else: # Default behavior to use latest version _print(( - ">>> Finding latest version compatible " + ">>> Finding latest version " f"with [ {installed_version} ]")) openpype_version = bootstrap.find_latest_openpype_version( use_staging) @@ -947,7 +947,12 @@ def _boot_print_versions(use_staging, local_version, openpype_root): openpype_versions = bootstrap.find_openpype( include_zips=True, staging=use_staging, - compatible_with=compatible_with) + ) + openpype_versions = [ + version for version in openpype_versions + if version.is_compatible( + OpenPypeVersion.get_installed_version()) + ] list_versions(openpype_versions, local_version) From ae491af33b234f6ef7130c7f52f8a9e67cd032a4 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 01:58:36 +0300 Subject: [PATCH 110/155] Adjust schema to include all lights flag. --- openpype/settings/defaults/project_settings/maya.json | 5 +++-- .../projects_schema/schemas/schema_maya_render_settings.json | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index ac0f161cf2..c95d47d576 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -33,7 +33,8 @@ }, "RenderSettings": { "apply_render_settings": true, - "default_render_image_folder": "", + "default_render_image_folder": "renders", + "enable_all_lights": false, "aov_separator": "underscore", "reset_current_frame": false, "arnold_renderer": { @@ -976,4 +977,4 @@ "ValidateNoAnimation": false } } -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json index af197604f8..6ee02ca78f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json @@ -14,6 +14,11 @@ "key": "default_render_image_folder", "label": "Default render image folder" }, + { + "type": "boolean", + "key": "enable_all_lights", + "label": "Include all lights in Render Setup Layers by default" + }, { "key": "aov_separator", "label": "AOV Separator character", From bbe7bc2fdb533375d9acc48a8c6b2f5c1538ecc1 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 01:59:20 +0300 Subject: [PATCH 111/155] Include `RenderSetupIncludeLights` flag in plugin info, grab value from render instance. --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index f253ceb21a..7966861358 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -62,6 +62,7 @@ payload_skeleton_template = { "RenderLayer": None, # Render only this layer "Renderer": None, "ProjectPath": None, # Resolve relative references + "RenderSetupIncludeLights": None, # Include all lights flag. }, "AuxFiles": [] # Mandatory for Deadline, may be empty } @@ -413,8 +414,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # Gather needed data ------------------------------------------------ default_render_file = instance.context.data.get('project_settings')\ .get('maya')\ - .get('create')\ - .get('CreateRender')\ + .get('RenderSettings')\ .get('default_render_image_folder') filename = os.path.basename(filepath) comment = context.data.get("comment", "") @@ -505,6 +505,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.payload_skeleton["JobInfo"]["Comment"] = comment self.payload_skeleton["PluginInfo"]["RenderLayer"] = renderlayer + self.payload_skeleton["PluginInfo"]["RenderSetupIncludeLights"] = instance.data.get("renderSetupIncludeLights") # noqa # Adding file dependencies. dependencies = instance.context.data["fileDependencies"] dependencies.append(filepath) From d7aba60460ce19af1c9a4c2bb629c967f8d06750 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 01:59:36 +0300 Subject: [PATCH 112/155] Validate lights flag --- .../hosts/maya/plugins/publish/validate_rendersettings.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 1dab3274a0..93ef7d7af7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -242,6 +242,14 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): instance.context.data["project_settings"]["maya"]["publish"]["ValidateRenderSettings"].get( # noqa: E501 "{}_render_attributes".format(renderer)) or [] ) + settings_lights_flag = instance.context.data["project_settings"].get( + "maya", {}).get( + "RenderSettings", {}).get( + "enable_all_lights", {}) + + instance_lights_flag = instance.data.get("renderSetupIncludeLights") + if settings_lights_flag != instance_lights_flag: + cls.log.warning('Instance flag for "Render Setup Include Lights" is set to {0} and Settings flag is set to {1}'.format(instance_lights_flag, settings_lights_flag)) # noqa # go through definitions and test if such node.attribute exists. # if so, compare its value from the one required. From 5322527226344498aec2b830847b42a60e91eca8 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 01:59:56 +0300 Subject: [PATCH 113/155] add flag attribute to render creator --- openpype/hosts/maya/plugins/create/create_render.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index fbe670b1ea..2f09aaee87 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -71,7 +71,7 @@ class CreateRender(plugin.Creator): label = "Render" family = "rendering" icon = "eye" - + enable_all_lights = True _token = None _user = None _password = None @@ -220,6 +220,12 @@ class CreateRender(plugin.Creator): self.data["tilesY"] = 2 self.data["convertToScanline"] = False self.data["useReferencedAovs"] = False + self.data["renderSetupIncludeLights"] = ( + self._project_settings.get( + "maya", {}).get( + "RenderSettings", {}).get( + "enable_all_lights", {}) + ) # Disable for now as this feature is not working yet # self.data["assScene"] = False From 5146c5a7e7f83032ba5d512a6861fb8f9b1b47f1 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 02:00:15 +0300 Subject: [PATCH 114/155] Add flag to collector, fix settings path bug --- openpype/hosts/maya/api/lib_rendersettings.py | 3 +- .../maya/plugins/publish/collect_render.py | 8 +++-- .../publish/validate_render_image_rule.py | 31 +++++++++++-------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 9aea55a03b..7cd2193086 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -60,8 +60,7 @@ class RenderSettings(object): try: aov_separator = self._aov_chars[( self._project_settings["maya"] - ["create"] - ["CreateRender"] + ["RenderSettings"] ["aov_separator"] )] except KeyError: diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e132cffe53..7035da2ec7 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -202,8 +202,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): aov_dict = {} default_render_file = context.data.get('project_settings')\ .get('maya')\ - .get('create')\ - .get('CreateRender')\ + .get('RenderSettings')\ .get('default_render_image_folder') or "" # replace relative paths with absolute. Render products are # returned as list of dictionaries. @@ -318,7 +317,10 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "useReferencedAovs": render_instance.data.get( "useReferencedAovs") or render_instance.data.get( "vrayUseReferencedAovs") or False, - "aovSeparator": layer_render_products.layer_data.aov_separator # noqa: E501 + "aovSeparator": layer_render_products.layer_data.aov_separator, # noqa: E501 + "renderSetupIncludeLights": render_instance.data.get( + "renderSetupIncludeLights" + ) } # Collect Deadline url if Deadline module is enabled diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index 642ca9e25d..353d0ad63a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -1,6 +1,6 @@ import maya.mel as mel -import pymel.core as pm +from maya import cmds import pyblish.api import openpype.api @@ -11,8 +11,10 @@ def get_file_rule(rule): class ValidateRenderImageRule(pyblish.api.InstancePlugin): - """Validates "images" file rule is set to "renders/" - + """Validates Maya Workpace "images" file rule matches project settings. + This validates against the configured default render image folder: + Studio Settings > Project > Maya > + Render Settings > Default render image folder. """ order = openpype.api.ValidateContentsOrder @@ -22,25 +24,28 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): actions = [openpype.api.RepairAction] def process(self, instance): + required_images_rule = self.get_default_render_image_folder(instance) + current_images_rule = cmds.workspace(fileRuleEntry="images") - default_render_file = self.get_default_render_image_folder(instance) - - assert get_file_rule("images") == default_render_file, ( - "Workspace's `images` file rule must be set to: {}".format( - default_render_file + assert current_images_rule == required_images_rule, ( + "Invalid workspace `images` file rule value: '{}'. " + "Must be set to: '{}'".format( + current_images_rule, required_images_rule ) ) @classmethod def repair(cls, instance): - default = cls.get_default_render_image_folder(instance) - pm.workspace.fileRules["images"] = default - pm.system.Workspace.save() + required_images_rule = cls.get_default_render_image_folder(instance) + current_images_rule = cmds.workspace(fileRuleEntry="images") + + if current_images_rule != required_images_rule: + cmds.workspace(fileRule=("images", required_images_rule)) + cmds.workspace(saveWorkspace=True) @staticmethod def get_default_render_image_folder(instance): return instance.context.data.get('project_settings')\ .get('maya') \ - .get('create') \ - .get('CreateRender') \ + .get('RenderSettings') \ .get('default_render_image_folder') From 8fcf5ffa28ae615219d2d3a31b0419bec3a746b6 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 02:19:02 +0300 Subject: [PATCH 115/155] Revert "Add flag to collector, fix settings path bug" This reverts part of commit 5146c5a7e7f83032ba5d512a6861fb8f9b1b47f1. --- .../publish/validate_render_image_rule.py | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index 353d0ad63a..642ca9e25d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -1,6 +1,6 @@ import maya.mel as mel +import pymel.core as pm -from maya import cmds import pyblish.api import openpype.api @@ -11,10 +11,8 @@ def get_file_rule(rule): class ValidateRenderImageRule(pyblish.api.InstancePlugin): - """Validates Maya Workpace "images" file rule matches project settings. - This validates against the configured default render image folder: - Studio Settings > Project > Maya > - Render Settings > Default render image folder. + """Validates "images" file rule is set to "renders/" + """ order = openpype.api.ValidateContentsOrder @@ -24,28 +22,25 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): actions = [openpype.api.RepairAction] def process(self, instance): - required_images_rule = self.get_default_render_image_folder(instance) - current_images_rule = cmds.workspace(fileRuleEntry="images") - assert current_images_rule == required_images_rule, ( - "Invalid workspace `images` file rule value: '{}'. " - "Must be set to: '{}'".format( - current_images_rule, required_images_rule + default_render_file = self.get_default_render_image_folder(instance) + + assert get_file_rule("images") == default_render_file, ( + "Workspace's `images` file rule must be set to: {}".format( + default_render_file ) ) @classmethod def repair(cls, instance): - required_images_rule = cls.get_default_render_image_folder(instance) - current_images_rule = cmds.workspace(fileRuleEntry="images") - - if current_images_rule != required_images_rule: - cmds.workspace(fileRule=("images", required_images_rule)) - cmds.workspace(saveWorkspace=True) + default = cls.get_default_render_image_folder(instance) + pm.workspace.fileRules["images"] = default + pm.system.Workspace.save() @staticmethod def get_default_render_image_folder(instance): return instance.context.data.get('project_settings')\ .get('maya') \ - .get('RenderSettings') \ + .get('create') \ + .get('CreateRender') \ .get('default_render_image_folder') From d2f9c100c35edbe9cafd59db6e732c9ba058e309 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 02:19:42 +0300 Subject: [PATCH 116/155] Fix correct path bug --- .../hosts/maya/plugins/publish/validate_render_image_rule.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index 642ca9e25d..0abcf2f12a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -41,6 +41,5 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): def get_default_render_image_folder(instance): return instance.context.data.get('project_settings')\ .get('maya') \ - .get('create') \ - .get('CreateRender') \ + .get('RenderSettings') \ .get('default_render_image_folder') From 553fcdff538178019d76d73ccb0b83119a816ef4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 13:22:25 +0200 Subject: [PATCH 117/155] added python 2 compatible attrs to vendor --- .../vendor/python/python_2/attr/__init__.py | 80 + .../vendor/python/python_2/attr/__init__.pyi | 484 +++ openpype/vendor/python/python_2/attr/_cmp.py | 154 + openpype/vendor/python/python_2/attr/_cmp.pyi | 13 + .../vendor/python/python_2/attr/_compat.py | 261 ++ .../vendor/python/python_2/attr/_config.py | 33 + .../vendor/python/python_2/attr/_funcs.py | 422 +++ openpype/vendor/python/python_2/attr/_make.py | 3173 +++++++++++++++++ .../vendor/python/python_2/attr/_next_gen.py | 216 ++ .../python/python_2/attr/_version_info.py | 87 + .../python/python_2/attr/_version_info.pyi | 9 + .../vendor/python/python_2/attr/converters.py | 155 + .../python/python_2/attr/converters.pyi | 13 + .../vendor/python/python_2/attr/exceptions.py | 94 + .../python/python_2/attr/exceptions.pyi | 17 + .../vendor/python/python_2/attr/filters.py | 54 + .../vendor/python/python_2/attr/filters.pyi | 6 + openpype/vendor/python/python_2/attr/py.typed | 0 .../vendor/python/python_2/attr/setters.py | 79 + .../vendor/python/python_2/attr/setters.pyi | 19 + .../vendor/python/python_2/attr/validators.py | 561 +++ .../python/python_2/attr/validators.pyi | 78 + .../vendor/python/python_2/attrs/__init__.py | 70 + .../vendor/python/python_2/attrs/__init__.pyi | 63 + .../python/python_2/attrs/converters.py | 3 + .../python/python_2/attrs/exceptions.py | 3 + .../vendor/python/python_2/attrs/filters.py | 3 + .../vendor/python/python_2/attrs/py.typed | 0 .../vendor/python/python_2/attrs/setters.py | 3 + .../python/python_2/attrs/validators.py | 3 + 30 files changed, 6156 insertions(+) create mode 100644 openpype/vendor/python/python_2/attr/__init__.py create mode 100644 openpype/vendor/python/python_2/attr/__init__.pyi create mode 100644 openpype/vendor/python/python_2/attr/_cmp.py create mode 100644 openpype/vendor/python/python_2/attr/_cmp.pyi create mode 100644 openpype/vendor/python/python_2/attr/_compat.py create mode 100644 openpype/vendor/python/python_2/attr/_config.py create mode 100644 openpype/vendor/python/python_2/attr/_funcs.py create mode 100644 openpype/vendor/python/python_2/attr/_make.py create mode 100644 openpype/vendor/python/python_2/attr/_next_gen.py create mode 100644 openpype/vendor/python/python_2/attr/_version_info.py create mode 100644 openpype/vendor/python/python_2/attr/_version_info.pyi create mode 100644 openpype/vendor/python/python_2/attr/converters.py create mode 100644 openpype/vendor/python/python_2/attr/converters.pyi create mode 100644 openpype/vendor/python/python_2/attr/exceptions.py create mode 100644 openpype/vendor/python/python_2/attr/exceptions.pyi create mode 100644 openpype/vendor/python/python_2/attr/filters.py create mode 100644 openpype/vendor/python/python_2/attr/filters.pyi create mode 100644 openpype/vendor/python/python_2/attr/py.typed create mode 100644 openpype/vendor/python/python_2/attr/setters.py create mode 100644 openpype/vendor/python/python_2/attr/setters.pyi create mode 100644 openpype/vendor/python/python_2/attr/validators.py create mode 100644 openpype/vendor/python/python_2/attr/validators.pyi create mode 100644 openpype/vendor/python/python_2/attrs/__init__.py create mode 100644 openpype/vendor/python/python_2/attrs/__init__.pyi create mode 100644 openpype/vendor/python/python_2/attrs/converters.py create mode 100644 openpype/vendor/python/python_2/attrs/exceptions.py create mode 100644 openpype/vendor/python/python_2/attrs/filters.py create mode 100644 openpype/vendor/python/python_2/attrs/py.typed create mode 100644 openpype/vendor/python/python_2/attrs/setters.py create mode 100644 openpype/vendor/python/python_2/attrs/validators.py diff --git a/openpype/vendor/python/python_2/attr/__init__.py b/openpype/vendor/python/python_2/attr/__init__.py new file mode 100644 index 0000000000..f95c96dd57 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/__init__.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import sys + +from functools import partial + +from . import converters, exceptions, filters, setters, validators +from ._cmp import cmp_using +from ._config import get_run_validators, set_run_validators +from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types +from ._make import ( + NOTHING, + Attribute, + Factory, + attrib, + attrs, + fields, + fields_dict, + make_class, + validate, +) +from ._version_info import VersionInfo + + +__version__ = "21.4.0" +__version_info__ = VersionInfo._from_version_string(__version__) + +__title__ = "attrs" +__description__ = "Classes Without Boilerplate" +__url__ = "https://www.attrs.org/" +__uri__ = __url__ +__doc__ = __description__ + " <" + __uri__ + ">" + +__author__ = "Hynek Schlawack" +__email__ = "hs@ox.cx" + +__license__ = "MIT" +__copyright__ = "Copyright (c) 2015 Hynek Schlawack" + + +s = attributes = attrs +ib = attr = attrib +dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + +__all__ = [ + "Attribute", + "Factory", + "NOTHING", + "asdict", + "assoc", + "astuple", + "attr", + "attrib", + "attributes", + "attrs", + "cmp_using", + "converters", + "evolve", + "exceptions", + "fields", + "fields_dict", + "filters", + "get_run_validators", + "has", + "ib", + "make_class", + "resolve_types", + "s", + "set_run_validators", + "setters", + "validate", + "validators", +] + +if sys.version_info[:2] >= (3, 6): + from ._next_gen import define, field, frozen, mutable # noqa: F401 + + __all__.extend(("define", "field", "frozen", "mutable")) diff --git a/openpype/vendor/python/python_2/attr/__init__.pyi b/openpype/vendor/python/python_2/attr/__init__.pyi new file mode 100644 index 0000000000..c0a2126503 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/__init__.pyi @@ -0,0 +1,484 @@ +import sys + +from typing import ( + Any, + Callable, + Dict, + Generic, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + overload, +) + +# `import X as X` is required to make these public +from . import converters as converters +from . import exceptions as exceptions +from . import filters as filters +from . import setters as setters +from . import validators as validators +from ._version_info import VersionInfo + +__version__: str +__version_info__: VersionInfo +__title__: str +__description__: str +__url__: str +__uri__: str +__author__: str +__email__: str +__license__: str +__copyright__: str + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_EqOrderType = Union[bool, Callable[[Any], Any]] +_ValidatorType = Callable[[Any, Attribute[_T], _T], Any] +_ConverterType = Callable[[Any], Any] +_FilterType = Callable[[Attribute[_T], _T], bool] +_ReprType = Callable[[Any], str] +_ReprArgType = Union[bool, _ReprType] +_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any] +_OnSetAttrArgType = Union[ + _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType +] +_FieldTransformer = Callable[ + [type, List[Attribute[Any]]], List[Attribute[Any]] +] +_CompareWithType = Callable[[Any, Any], bool] +# FIXME: in reality, if multiple validators are passed they must be in a list +# or tuple, but those are invariant and so would prevent subtypes of +# _ValidatorType from working when passed in a list or tuple. +_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] + +# _make -- + +NOTHING: object + +# NOTE: Factory lies about its return type to make this possible: +# `x: List[int] # = Factory(list)` +# Work around mypy issue #4554 in the common case by using an overload. +if sys.version_info >= (3, 8): + from typing import Literal + @overload + def Factory(factory: Callable[[], _T]) -> _T: ... + @overload + def Factory( + factory: Callable[[Any], _T], + takes_self: Literal[True], + ) -> _T: ... + @overload + def Factory( + factory: Callable[[], _T], + takes_self: Literal[False], + ) -> _T: ... + +else: + @overload + def Factory(factory: Callable[[], _T]) -> _T: ... + @overload + def Factory( + factory: Union[Callable[[Any], _T], Callable[[], _T]], + takes_self: bool = ..., + ) -> _T: ... + +# Static type inference support via __dataclass_transform__ implemented as per: +# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md +# This annotation must be applied to all overloads of "define" and "attrs" +# +# NOTE: This is a typing construct and does not exist at runtime. Extensions +# wrapping attrs decorators should declare a separate __dataclass_transform__ +# signature in the extension module using the specification linked above to +# provide pyright support. +def __dataclass_transform__( + *, + eq_default: bool = True, + order_default: bool = False, + kw_only_default: bool = False, + field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), +) -> Callable[[_T], _T]: ... + +class Attribute(Generic[_T]): + name: str + default: Optional[_T] + validator: Optional[_ValidatorType[_T]] + repr: _ReprArgType + cmp: _EqOrderType + eq: _EqOrderType + order: _EqOrderType + hash: Optional[bool] + init: bool + converter: Optional[_ConverterType] + metadata: Dict[Any, Any] + type: Optional[Type[_T]] + kw_only: bool + on_setattr: _OnSetAttrType + def evolve(self, **changes: Any) -> "Attribute[Any]": ... + +# NOTE: We had several choices for the annotation to use for type arg: +# 1) Type[_T] +# - Pros: Handles simple cases correctly +# - Cons: Might produce less informative errors in the case of conflicting +# TypeVars e.g. `attr.ib(default='bad', type=int)` +# 2) Callable[..., _T] +# - Pros: Better error messages than #1 for conflicting TypeVars +# - Cons: Terrible error messages for validator checks. +# e.g. attr.ib(type=int, validator=validate_str) +# -> error: Cannot infer function type argument +# 3) type (and do all of the work in the mypy plugin) +# - Pros: Simple here, and we could customize the plugin with our own errors. +# - Cons: Would need to write mypy plugin code to handle all the cases. +# We chose option #1. + +# `attr` lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) +# +# This form catches explicit None or no default but with no other arguments +# returns Any. +@overload +def attrib( + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def attrib( + default: None = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def attrib( + default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def attrib( + default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: object = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... +@overload +def field( + *, + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def field( + *, + default: None = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def field( + *, + default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def field( + *, + default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... +@overload +@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +def attrs( + maybe_cls: _C, + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +def attrs( + maybe_cls: None = ..., + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... +@overload +@__dataclass_transform__(field_descriptors=(attrib, field)) +def define( + maybe_cls: _C, + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@__dataclass_transform__(field_descriptors=(attrib, field)) +def define( + maybe_cls: None = ..., + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... + +mutable = define +frozen = define # they differ only in their defaults + +# TODO: add support for returning NamedTuple from the mypy plugin +class _Fields(Tuple[Attribute[Any], ...]): + def __getattr__(self, name: str) -> Attribute[Any]: ... + +def fields(cls: type) -> _Fields: ... +def fields_dict(cls: type) -> Dict[str, Attribute[Any]]: ... +def validate(inst: Any) -> None: ... +def resolve_types( + cls: _C, + globalns: Optional[Dict[str, Any]] = ..., + localns: Optional[Dict[str, Any]] = ..., + attribs: Optional[List[Attribute[Any]]] = ..., +) -> _C: ... + +# TODO: add support for returning a proper attrs class from the mypy plugin +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', +# [attr.ib()])` is valid +def make_class( + name: str, + attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], + bases: Tuple[type, ...] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + collect_by_mro: bool = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., +) -> type: ... + +# _funcs -- + +# TODO: add support for returning TypedDict from the mypy plugin +# FIXME: asdict/astuple do not honor their factory args. Waiting on one of +# these: +# https://github.com/python/mypy/issues/4236 +# https://github.com/python/typing/issues/253 +# XXX: remember to fix attrs.asdict/astuple too! +def asdict( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + dict_factory: Type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Optional[ + Callable[[type, Attribute[Any], Any], Any] + ] = ..., + tuple_keys: Optional[bool] = ..., +) -> Dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + tuple_factory: Type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> Tuple[Any, ...]: ... +def has(cls: type) -> bool: ... +def assoc(inst: _T, **changes: Any) -> _T: ... +def evolve(inst: _T, **changes: Any) -> _T: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases -- + +s = attributes = attrs +ib = attr = attrib +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/openpype/vendor/python/python_2/attr/_cmp.py b/openpype/vendor/python/python_2/attr/_cmp.py new file mode 100644 index 0000000000..6cffa4dbab --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_cmp.py @@ -0,0 +1,154 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import functools + +from ._compat import new_class +from ._make import _make_ne + + +_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="} + + +def cmp_using( + eq=None, + lt=None, + le=None, + gt=None, + ge=None, + require_same_type=True, + class_name="Comparable", +): + """ + Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and + ``cmp`` arguments to customize field comparison. + + The resulting class will have a full set of ordering methods if + at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + + :param Optional[callable] eq: `callable` used to evaluate equality + of two objects. + :param Optional[callable] lt: `callable` used to evaluate whether + one object is less than another object. + :param Optional[callable] le: `callable` used to evaluate whether + one object is less than or equal to another object. + :param Optional[callable] gt: `callable` used to evaluate whether + one object is greater than another object. + :param Optional[callable] ge: `callable` used to evaluate whether + one object is greater than or equal to another object. + + :param bool require_same_type: When `True`, equality and ordering methods + will return `NotImplemented` if objects are not of the same type. + + :param Optional[str] class_name: Name of class. Defaults to 'Comparable'. + + See `comparison` for more details. + + .. versionadded:: 21.1.0 + """ + + body = { + "__slots__": ["value"], + "__init__": _make_init(), + "_requirements": [], + "_is_comparable_to": _is_comparable_to, + } + + # Add operations. + num_order_functions = 0 + has_eq_function = False + + if eq is not None: + has_eq_function = True + body["__eq__"] = _make_operator("eq", eq) + body["__ne__"] = _make_ne() + + if lt is not None: + num_order_functions += 1 + body["__lt__"] = _make_operator("lt", lt) + + if le is not None: + num_order_functions += 1 + body["__le__"] = _make_operator("le", le) + + if gt is not None: + num_order_functions += 1 + body["__gt__"] = _make_operator("gt", gt) + + if ge is not None: + num_order_functions += 1 + body["__ge__"] = _make_operator("ge", ge) + + type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body)) + + # Add same type requirement. + if require_same_type: + type_._requirements.append(_check_same_type) + + # Add total ordering if at least one operation was defined. + if 0 < num_order_functions < 4: + if not has_eq_function: + # functools.total_ordering requires __eq__ to be defined, + # so raise early error here to keep a nice stack. + raise ValueError( + "eq must be define is order to complete ordering from " + "lt, le, gt, ge." + ) + type_ = functools.total_ordering(type_) + + return type_ + + +def _make_init(): + """ + Create __init__ method. + """ + + def __init__(self, value): + """ + Initialize object with *value*. + """ + self.value = value + + return __init__ + + +def _make_operator(name, func): + """ + Create operator method. + """ + + def method(self, other): + if not self._is_comparable_to(other): + return NotImplemented + + result = func(self.value, other.value) + if result is NotImplemented: + return NotImplemented + + return result + + method.__name__ = "__%s__" % (name,) + method.__doc__ = "Return a %s b. Computed by attrs." % ( + _operation_names[name], + ) + + return method + + +def _is_comparable_to(self, other): + """ + Check whether `other` is comparable to `self`. + """ + for func in self._requirements: + if not func(self, other): + return False + return True + + +def _check_same_type(self, other): + """ + Return True if *self* and *other* are of the same type, False otherwise. + """ + return other.value.__class__ is self.value.__class__ diff --git a/openpype/vendor/python/python_2/attr/_cmp.pyi b/openpype/vendor/python/python_2/attr/_cmp.pyi new file mode 100644 index 0000000000..e71aaff7a1 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_cmp.pyi @@ -0,0 +1,13 @@ +from typing import Type + +from . import _CompareWithType + +def cmp_using( + eq: Optional[_CompareWithType], + lt: Optional[_CompareWithType], + le: Optional[_CompareWithType], + gt: Optional[_CompareWithType], + ge: Optional[_CompareWithType], + require_same_type: bool, + class_name: str, +) -> Type: ... diff --git a/openpype/vendor/python/python_2/attr/_compat.py b/openpype/vendor/python/python_2/attr/_compat.py new file mode 100644 index 0000000000..dc0cb02b64 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_compat.py @@ -0,0 +1,261 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import platform +import sys +import threading +import types +import warnings + + +PY2 = sys.version_info[0] == 2 +PYPY = platform.python_implementation() == "PyPy" +PY36 = sys.version_info[:2] >= (3, 6) +HAS_F_STRINGS = PY36 +PY310 = sys.version_info[:2] >= (3, 10) + + +if PYPY or PY36: + ordered_dict = dict +else: + from collections import OrderedDict + + ordered_dict = OrderedDict + + +if PY2: + from collections import Mapping, Sequence + + from UserDict import IterableUserDict + + # We 'bundle' isclass instead of using inspect as importing inspect is + # fairly expensive (order of 10-15 ms for a modern machine in 2016) + def isclass(klass): + return isinstance(klass, (type, types.ClassType)) + + def new_class(name, bases, kwds, exec_body): + """ + A minimal stub of types.new_class that we need for make_class. + """ + ns = {} + exec_body(ns) + + return type(name, bases, ns) + + # TYPE is used in exceptions, repr(int) is different on Python 2 and 3. + TYPE = "type" + + def iteritems(d): + return d.iteritems() + + # Python 2 is bereft of a read-only dict proxy, so we make one! + class ReadOnlyDict(IterableUserDict): + """ + Best-effort read-only dict wrapper. + """ + + def __setitem__(self, key, val): + # We gently pretend we're a Python 3 mappingproxy. + raise TypeError( + "'mappingproxy' object does not support item assignment" + ) + + def update(self, _): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'update'" + ) + + def __delitem__(self, _): + # We gently pretend we're a Python 3 mappingproxy. + raise TypeError( + "'mappingproxy' object does not support item deletion" + ) + + def clear(self): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'clear'" + ) + + def pop(self, key, default=None): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'pop'" + ) + + def popitem(self): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'popitem'" + ) + + def setdefault(self, key, default=None): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'setdefault'" + ) + + def __repr__(self): + # Override to be identical to the Python 3 version. + return "mappingproxy(" + repr(self.data) + ")" + + def metadata_proxy(d): + res = ReadOnlyDict() + res.data.update(d) # We blocked update, so we have to do it like this. + return res + + def just_warn(*args, **kw): # pragma: no cover + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + +else: # Python 3 and later. + from collections.abc import Mapping, Sequence # noqa + + def just_warn(*args, **kw): + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + warnings.warn( + "Running interpreter doesn't sufficiently support code object " + "introspection. Some features like bare super() or accessing " + "__class__ will not work with slotted classes.", + RuntimeWarning, + stacklevel=2, + ) + + def isclass(klass): + return isinstance(klass, type) + + TYPE = "class" + + def iteritems(d): + return d.items() + + new_class = types.new_class + + def metadata_proxy(d): + return types.MappingProxyType(dict(d)) + + +def make_set_closure_cell(): + """Return a function of two arguments (cell, value) which sets + the value stored in the closure cell `cell` to `value`. + """ + # pypy makes this easy. (It also supports the logic below, but + # why not do the easy/fast thing?) + if PYPY: + + def set_closure_cell(cell, value): + cell.__setstate__((value,)) + + return set_closure_cell + + # Otherwise gotta do it the hard way. + + # Create a function that will set its first cellvar to `value`. + def set_first_cellvar_to(value): + x = value + return + + # This function will be eliminated as dead code, but + # not before its reference to `x` forces `x` to be + # represented as a closure cell rather than a local. + def force_x_to_be_a_cell(): # pragma: no cover + return x + + try: + # Extract the code object and make sure our assumptions about + # the closure behavior are correct. + if PY2: + co = set_first_cellvar_to.func_code + else: + co = set_first_cellvar_to.__code__ + if co.co_cellvars != ("x",) or co.co_freevars != (): + raise AssertionError # pragma: no cover + + # Convert this code object to a code object that sets the + # function's first _freevar_ (not cellvar) to the argument. + if sys.version_info >= (3, 8): + # CPython 3.8+ has an incompatible CodeType signature + # (added a posonlyargcount argument) but also added + # CodeType.replace() to do this without counting parameters. + set_first_freevar_code = co.replace( + co_cellvars=co.co_freevars, co_freevars=co.co_cellvars + ) + else: + args = [co.co_argcount] + if not PY2: + args.append(co.co_kwonlyargcount) + args.extend( + [ + co.co_nlocals, + co.co_stacksize, + co.co_flags, + co.co_code, + co.co_consts, + co.co_names, + co.co_varnames, + co.co_filename, + co.co_name, + co.co_firstlineno, + co.co_lnotab, + # These two arguments are reversed: + co.co_cellvars, + co.co_freevars, + ] + ) + set_first_freevar_code = types.CodeType(*args) + + def set_closure_cell(cell, value): + # Create a function using the set_first_freevar_code, + # whose first closure cell is `cell`. Calling it will + # change the value of that cell. + setter = types.FunctionType( + set_first_freevar_code, {}, "setter", (), (cell,) + ) + # And call it to set the cell. + setter(value) + + # Make sure it works on this interpreter: + def make_func_with_cell(): + x = None + + def func(): + return x # pragma: no cover + + return func + + if PY2: + cell = make_func_with_cell().func_closure[0] + else: + cell = make_func_with_cell().__closure__[0] + set_closure_cell(cell, 100) + if cell.cell_contents != 100: + raise AssertionError # pragma: no cover + + except Exception: + return just_warn + else: + return set_closure_cell + + +set_closure_cell = make_set_closure_cell() + +# Thread-local global to track attrs instances which are already being repr'd. +# This is needed because there is no other (thread-safe) way to pass info +# about the instances that are already being repr'd through the call stack +# in order to ensure we don't perform infinite recursion. +# +# For instance, if an instance contains a dict which contains that instance, +# we need to know that we're already repr'ing the outside instance from within +# the dict's repr() call. +# +# This lives here rather than in _make.py so that the functions in _make.py +# don't have a direct reference to the thread-local in their globals dict. +# If they have such a reference, it breaks cloudpickle. +repr_context = threading.local() diff --git a/openpype/vendor/python/python_2/attr/_config.py b/openpype/vendor/python/python_2/attr/_config.py new file mode 100644 index 0000000000..fc9be29d00 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_config.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + + +__all__ = ["set_run_validators", "get_run_validators"] + +_run_validators = True + + +def set_run_validators(run): + """ + Set whether or not validators are run. By default, they are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` + instead. + """ + if not isinstance(run, bool): + raise TypeError("'run' must be bool.") + global _run_validators + _run_validators = run + + +def get_run_validators(): + """ + Return whether or not validators are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` + instead. + """ + return _run_validators diff --git a/openpype/vendor/python/python_2/attr/_funcs.py b/openpype/vendor/python/python_2/attr/_funcs.py new file mode 100644 index 0000000000..4c90085a40 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_funcs.py @@ -0,0 +1,422 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import copy + +from ._compat import iteritems +from ._make import NOTHING, _obj_setattr, fields +from .exceptions import AttrsAttributeNotFoundError + + +def asdict( + inst, + recurse=True, + filter=None, + dict_factory=dict, + retain_collection_types=False, + value_serializer=None, +): + """ + Return the ``attrs`` attribute values of *inst* as a dict. + + Optionally recurse into other ``attrs``-decorated classes. + + :param inst: Instance of an ``attrs``-decorated class. + :param bool recurse: Recurse into classes that are also + ``attrs``-decorated. + :param callable filter: A callable whose return code determines whether an + attribute or element is included (``True``) or dropped (``False``). Is + called with the `attrs.Attribute` as the first argument and the + value as the second argument. + :param callable dict_factory: A callable to produce dictionaries from. For + example, to produce ordered dictionaries instead of normal Python + dictionaries, pass in ``collections.OrderedDict``. + :param bool retain_collection_types: Do not convert to ``list`` when + encountering an attribute whose type is ``tuple`` or ``set``. Only + meaningful if ``recurse`` is ``True``. + :param Optional[callable] value_serializer: A hook that is called for every + attribute or dict key/value. It receives the current instance, field + and value and must return the (updated) value. The hook is run *after* + the optional *filter* has been applied. + + :rtype: return type of *dict_factory* + + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 16.0.0 *dict_factory* + .. versionadded:: 16.1.0 *retain_collection_types* + .. versionadded:: 20.3.0 *value_serializer* + .. versionadded:: 21.3.0 If a dict has a collection for a key, it is + serialized as a tuple. + """ + attrs = fields(inst.__class__) + rv = dict_factory() + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + + if value_serializer is not None: + v = value_serializer(inst, a, v) + + if recurse is True: + if has(v.__class__): + rv[a.name] = asdict( + v, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif isinstance(v, (tuple, list, set, frozenset)): + cf = v.__class__ if retain_collection_types is True else list + rv[a.name] = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in v + ] + ) + elif isinstance(v, dict): + df = dict_factory + rv[a.name] = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in iteritems(v) + ) + else: + rv[a.name] = v + else: + rv[a.name] = v + return rv + + +def _asdict_anything( + val, + is_key, + filter, + dict_factory, + retain_collection_types, + value_serializer, +): + """ + ``asdict`` only works on attrs instances, this works on anything. + """ + if getattr(val.__class__, "__attrs_attrs__", None) is not None: + # Attrs class. + rv = asdict( + val, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif isinstance(val, (tuple, list, set, frozenset)): + if retain_collection_types is True: + cf = val.__class__ + elif is_key: + cf = tuple + else: + cf = list + + rv = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in val + ] + ) + elif isinstance(val, dict): + df = dict_factory + rv = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in iteritems(val) + ) + else: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + + return rv + + +def astuple( + inst, + recurse=True, + filter=None, + tuple_factory=tuple, + retain_collection_types=False, +): + """ + Return the ``attrs`` attribute values of *inst* as a tuple. + + Optionally recurse into other ``attrs``-decorated classes. + + :param inst: Instance of an ``attrs``-decorated class. + :param bool recurse: Recurse into classes that are also + ``attrs``-decorated. + :param callable filter: A callable whose return code determines whether an + attribute or element is included (``True``) or dropped (``False``). Is + called with the `attrs.Attribute` as the first argument and the + value as the second argument. + :param callable tuple_factory: A callable to produce tuples from. For + example, to produce lists instead of tuples. + :param bool retain_collection_types: Do not convert to ``list`` + or ``dict`` when encountering an attribute which type is + ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is + ``True``. + + :rtype: return type of *tuple_factory* + + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 16.2.0 + """ + attrs = fields(inst.__class__) + rv = [] + retain = retain_collection_types # Very long. :/ + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + if recurse is True: + if has(v.__class__): + rv.append( + astuple( + v, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + ) + elif isinstance(v, (tuple, list, set, frozenset)): + cf = v.__class__ if retain is True else list + rv.append( + cf( + [ + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(j.__class__) + else j + for j in v + ] + ) + ) + elif isinstance(v, dict): + df = v.__class__ if retain is True else dict + rv.append( + df( + ( + astuple( + kk, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(kk.__class__) + else kk, + astuple( + vv, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(vv.__class__) + else vv, + ) + for kk, vv in iteritems(v) + ) + ) + else: + rv.append(v) + else: + rv.append(v) + + return rv if tuple_factory is list else tuple_factory(rv) + + +def has(cls): + """ + Check whether *cls* is a class with ``attrs`` attributes. + + :param type cls: Class to introspect. + :raise TypeError: If *cls* is not a class. + + :rtype: bool + """ + return getattr(cls, "__attrs_attrs__", None) is not None + + +def assoc(inst, **changes): + """ + Copy *inst* and apply *changes*. + + :param inst: Instance of a class with ``attrs`` attributes. + :param changes: Keyword changes in the new copy. + + :return: A copy of inst with *changes* incorporated. + + :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't + be found on *cls*. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. deprecated:: 17.1.0 + Use `attrs.evolve` instead if you can. + This function will not be removed du to the slightly different approach + compared to `attrs.evolve`. + """ + import warnings + + warnings.warn( + "assoc is deprecated and will be removed after 2018/01.", + DeprecationWarning, + stacklevel=2, + ) + new = copy.copy(inst) + attrs = fields(inst.__class__) + for k, v in iteritems(changes): + a = getattr(attrs, k, NOTHING) + if a is NOTHING: + raise AttrsAttributeNotFoundError( + "{k} is not an attrs attribute on {cl}.".format( + k=k, cl=new.__class__ + ) + ) + _obj_setattr(new, k, v) + return new + + +def evolve(inst, **changes): + """ + Create a new instance, based on *inst* with *changes* applied. + + :param inst: Instance of a class with ``attrs`` attributes. + :param changes: Keyword changes in the new copy. + + :return: A copy of inst with *changes* incorporated. + + :raise TypeError: If *attr_name* couldn't be found in the class + ``__init__``. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 17.1.0 + """ + cls = inst.__class__ + attrs = fields(cls) + for a in attrs: + if not a.init: + continue + attr_name = a.name # To deal with private attributes. + init_name = attr_name if attr_name[0] != "_" else attr_name[1:] + if init_name not in changes: + changes[init_name] = getattr(inst, attr_name) + + return cls(**changes) + + +def resolve_types(cls, globalns=None, localns=None, attribs=None): + """ + Resolve any strings and forward annotations in type annotations. + + This is only required if you need concrete types in `Attribute`'s *type* + field. In other words, you don't need to resolve your types if you only + use them for static type checking. + + With no arguments, names will be looked up in the module in which the class + was created. If this is not what you want, e.g. if the name only exists + inside a method, you may pass *globalns* or *localns* to specify other + dictionaries in which to look up these names. See the docs of + `typing.get_type_hints` for more details. + + :param type cls: Class to resolve. + :param Optional[dict] globalns: Dictionary containing global variables. + :param Optional[dict] localns: Dictionary containing local variables. + :param Optional[list] attribs: List of attribs for the given class. + This is necessary when calling from inside a ``field_transformer`` + since *cls* is not an ``attrs`` class yet. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class and you didn't pass any attribs. + :raise NameError: If types cannot be resolved because of missing variables. + + :returns: *cls* so you can use this function also as a class decorator. + Please note that you have to apply it **after** `attrs.define`. That + means the decorator has to come in the line **before** `attrs.define`. + + .. versionadded:: 20.1.0 + .. versionadded:: 21.1.0 *attribs* + + """ + # Since calling get_type_hints is expensive we cache whether we've + # done it already. + if getattr(cls, "__attrs_types_resolved__", None) != cls: + import typing + + hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) + for field in fields(cls) if attribs is None else attribs: + if field.name in hints: + # Since fields have been frozen we must work around it. + _obj_setattr(field, "type", hints[field.name]) + # We store the class we resolved so that subclasses know they haven't + # been resolved. + cls.__attrs_types_resolved__ = cls + + # Return the class so you can use it as a decorator too. + return cls diff --git a/openpype/vendor/python/python_2/attr/_make.py b/openpype/vendor/python/python_2/attr/_make.py new file mode 100644 index 0000000000..d46f8a3e7a --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_make.py @@ -0,0 +1,3173 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import copy +import inspect +import linecache +import sys +import warnings + +from operator import itemgetter + +# We need to import _compat itself in addition to the _compat members to avoid +# having the thread-local in the globals here. +from . import _compat, _config, setters +from ._compat import ( + HAS_F_STRINGS, + PY2, + PY310, + PYPY, + isclass, + iteritems, + metadata_proxy, + new_class, + ordered_dict, + set_closure_cell, +) +from .exceptions import ( + DefaultAlreadySetError, + FrozenInstanceError, + NotAnAttrsClassError, + PythonTooOldError, + UnannotatedAttributeError, +) + + +if not PY2: + import typing + + +# This is used at least twice, so cache it here. +_obj_setattr = object.__setattr__ +_init_converter_pat = "__attr_converter_%s" +_init_factory_pat = "__attr_factory_{}" +_tuple_property_pat = ( + " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" +) +_classvar_prefixes = ( + "typing.ClassVar", + "t.ClassVar", + "ClassVar", + "typing_extensions.ClassVar", +) +# we don't use a double-underscore prefix because that triggers +# name mangling when trying to create a slot for the field +# (when slots=True) +_hash_cache_field = "_attrs_cached_hash" + +_empty_metadata_singleton = metadata_proxy({}) + +# Unique object for unequivocal getattr() defaults. +_sentinel = object() + +_ng_default_on_setattr = setters.pipe(setters.convert, setters.validate) + + +class _Nothing(object): + """ + Sentinel class to indicate the lack of a value when ``None`` is ambiguous. + + ``_Nothing`` is a singleton. There is only ever one of it. + + .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + """ + + _singleton = None + + def __new__(cls): + if _Nothing._singleton is None: + _Nothing._singleton = super(_Nothing, cls).__new__(cls) + return _Nothing._singleton + + def __repr__(self): + return "NOTHING" + + def __bool__(self): + return False + + def __len__(self): + return 0 # __bool__ for Python 2 + + +NOTHING = _Nothing() +""" +Sentinel to indicate the lack of a value when ``None`` is ambiguous. +""" + + +class _CacheHashWrapper(int): + """ + An integer subclass that pickles / copies as None + + This is used for non-slots classes with ``cache_hash=True``, to avoid + serializing a potentially (even likely) invalid hash value. Since ``None`` + is the default value for uncalculated hashes, whenever this is copied, + the copy's value for the hash should automatically reset. + + See GH #613 for more details. + """ + + if PY2: + # For some reason `type(None)` isn't callable in Python 2, but we don't + # actually need a constructor for None objects, we just need any + # available function that returns None. + def __reduce__(self, _none_constructor=getattr, _args=(0, "", None)): + return _none_constructor, _args + + else: + + def __reduce__(self, _none_constructor=type(None), _args=()): + return _none_constructor, _args + + +def attrib( + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=False, + eq=None, + order=None, + on_setattr=None, +): + """ + Create a new attribute on a class. + + .. warning:: + + Does *not* do anything unless the class is also decorated with + `attr.s`! + + :param default: A value that is used if an ``attrs``-generated ``__init__`` + is used and no value is passed while instantiating or the attribute is + excluded using ``init=False``. + + If the value is an instance of `attrs.Factory`, its callable will be + used to construct a new value (useful for mutable data types like lists + or dicts). + + If a default is not set (or set manually to `attrs.NOTHING`), a value + *must* be supplied when instantiating; otherwise a `TypeError` + will be raised. + + The default can also be set using decorator notation as shown below. + + :type default: Any value + + :param callable factory: Syntactic sugar for + ``default=attr.Factory(factory)``. + + :param validator: `callable` that is called by ``attrs``-generated + ``__init__`` methods after the instance has been initialized. They + receive the initialized instance, the :func:`~attrs.Attribute`, and the + passed value. + + The return value is *not* inspected so the validator has to throw an + exception itself. + + If a `list` is passed, its items are treated as validators and must + all pass. + + Validators can be globally disabled and re-enabled using + `get_run_validators`. + + The validator can also be set using decorator notation as shown below. + + :type validator: `callable` or a `list` of `callable`\\ s. + + :param repr: Include this attribute in the generated ``__repr__`` + method. If ``True``, include the attribute; if ``False``, omit it. By + default, the built-in ``repr()`` function is used. To override how the + attribute value is formatted, pass a ``callable`` that takes a single + value and returns a string. Note that the resulting string is used + as-is, i.e. it will be used directly *instead* of calling ``repr()`` + (the default). + :type repr: a `bool` or a `callable` to use a custom function. + + :param eq: If ``True`` (default), include this attribute in the + generated ``__eq__`` and ``__ne__`` methods that check two instances + for equality. To override how the attribute value is compared, + pass a ``callable`` that takes a single value and returns the value + to be compared. + :type eq: a `bool` or a `callable`. + + :param order: If ``True`` (default), include this attributes in the + generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. + To override how the attribute value is ordered, + pass a ``callable`` that takes a single value and returns the value + to be ordered. + :type order: a `bool` or a `callable`. + + :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the + same value. Must not be mixed with *eq* or *order*. + :type cmp: a `bool` or a `callable`. + + :param Optional[bool] hash: Include this attribute in the generated + ``__hash__`` method. If ``None`` (default), mirror *eq*'s value. This + is the correct behavior according the Python spec. Setting this value + to anything else than ``None`` is *discouraged*. + :param bool init: Include this attribute in the generated ``__init__`` + method. It is possible to set this to ``False`` and set a default + value. In that case this attributed is unconditionally initialized + with the specified default value or factory. + :param callable converter: `callable` that is called by + ``attrs``-generated ``__init__`` methods to convert attribute's value + to the desired format. It is given the passed-in value, and the + returned value will be used as the new value of the attribute. The + value is converted before being passed to the validator, if any. + :param metadata: An arbitrary mapping, to be used by third-party + components. See `extending_metadata`. + :param type: The type of the attribute. In Python 3.6 or greater, the + preferred method to specify the type is using a variable annotation + (see `PEP 526 `_). + This argument is provided for backward compatibility. + Regardless of the approach used, the type will be stored on + ``Attribute.type``. + + Please note that ``attrs`` doesn't do anything with this metadata by + itself. You can use it as part of your own code or for + `static type checking `. + :param kw_only: Make this attribute keyword-only (Python 3+) + in the generated ``__init__`` (if ``init`` is ``False``, this + parameter is ignored). + :param on_setattr: Allows to overwrite the *on_setattr* setting from + `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. + Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this + attribute -- regardless of the setting in `attr.s`. + :type on_setattr: `callable`, or a list of callables, or `None`, or + `attrs.setters.NO_OP` + + .. versionadded:: 15.2.0 *convert* + .. versionadded:: 16.3.0 *metadata* + .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. + .. versionchanged:: 17.1.0 + *hash* is ``None`` and therefore mirrors *eq* by default. + .. versionadded:: 17.3.0 *type* + .. deprecated:: 17.4.0 *convert* + .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated + *convert* to achieve consistency with other noun-based arguments. + .. versionadded:: 18.1.0 + ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. + .. versionadded:: 18.2.0 *kw_only* + .. versionchanged:: 19.2.0 *convert* keyword argument removed. + .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionchanged:: 21.1.0 *cmp* undeprecated + """ + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq, order, True + ) + + if hash is not None and hash is not True and hash is not False: + raise TypeError( + "Invalid value for hash. Must be True, False, or None." + ) + + if factory is not None: + if default is not NOTHING: + raise ValueError( + "The `default` and `factory` arguments are mutually " + "exclusive." + ) + if not callable(factory): + raise ValueError("The `factory` argument must be a callable.") + default = Factory(factory) + + if metadata is None: + metadata = {} + + # Apply syntactic sugar by auto-wrapping. + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + if validator and isinstance(validator, (list, tuple)): + validator = and_(*validator) + + if converter and isinstance(converter, (list, tuple)): + converter = pipe(*converter) + + return _CountingAttr( + default=default, + validator=validator, + repr=repr, + cmp=None, + hash=hash, + init=init, + converter=converter, + metadata=metadata, + type=type, + kw_only=kw_only, + eq=eq, + eq_key=eq_key, + order=order, + order_key=order_key, + on_setattr=on_setattr, + ) + + +def _compile_and_eval(script, globs, locs=None, filename=""): + """ + "Exec" the script with the given global (globs) and local (locs) variables. + """ + bytecode = compile(script, filename, "exec") + eval(bytecode, globs, locs) + + +def _make_method(name, script, filename, globs=None): + """ + Create the method with the script given and return the method object. + """ + locs = {} + if globs is None: + globs = {} + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + count = 1 + base_filename = filename + while True: + linecache_tuple = ( + len(script), + None, + script.splitlines(True), + filename, + ) + old_val = linecache.cache.setdefault(filename, linecache_tuple) + if old_val == linecache_tuple: + break + else: + filename = "{}-{}>".format(base_filename[:-1], count) + count += 1 + + _compile_and_eval(script, globs, locs, filename) + + return locs[name] + + +def _make_attr_tuple_class(cls_name, attr_names): + """ + Create a tuple subclass to hold `Attribute`s for an `attrs` class. + + The subclass is a bare tuple with properties for names. + + class MyClassAttributes(tuple): + __slots__ = () + x = property(itemgetter(0)) + """ + attr_class_name = "{}Attributes".format(cls_name) + attr_class_template = [ + "class {}(tuple):".format(attr_class_name), + " __slots__ = ()", + ] + if attr_names: + for i, attr_name in enumerate(attr_names): + attr_class_template.append( + _tuple_property_pat.format(index=i, attr_name=attr_name) + ) + else: + attr_class_template.append(" pass") + globs = {"_attrs_itemgetter": itemgetter, "_attrs_property": property} + _compile_and_eval("\n".join(attr_class_template), globs) + return globs[attr_class_name] + + +# Tuple class for extracted attributes from a class definition. +# `base_attrs` is a subset of `attrs`. +_Attributes = _make_attr_tuple_class( + "_Attributes", + [ + # all attributes to build dunder methods for + "attrs", + # attributes that have been inherited + "base_attrs", + # map inherited attributes to their originating classes + "base_attrs_map", + ], +) + + +def _is_class_var(annot): + """ + Check whether *annot* is a typing.ClassVar. + + The string comparison hack is used to avoid evaluating all string + annotations which would put attrs-based classes at a performance + disadvantage compared to plain old classes. + """ + annot = str(annot) + + # Annotation can be quoted. + if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): + annot = annot[1:-1] + + return annot.startswith(_classvar_prefixes) + + +def _has_own_attribute(cls, attrib_name): + """ + Check whether *cls* defines *attrib_name* (and doesn't just inherit it). + + Requires Python 3. + """ + attr = getattr(cls, attrib_name, _sentinel) + if attr is _sentinel: + return False + + for base_cls in cls.__mro__[1:]: + a = getattr(base_cls, attrib_name, None) + if attr is a: + return False + + return True + + +def _get_annotations(cls): + """ + Get annotations for *cls*. + """ + if _has_own_attribute(cls, "__annotations__"): + return cls.__annotations__ + + return {} + + +def _counter_getter(e): + """ + Key function for sorting to avoid re-creating a lambda for every class. + """ + return e[1].counter + + +def _collect_base_attrs(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in reversed(cls.__mro__[1:-1]): + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.inherited or a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + # For each name, only keep the freshest definition i.e. the furthest at the + # back. base_attr_map is fine because it gets overwritten with every new + # instance. + filtered = [] + seen = set() + for a in reversed(base_attrs): + if a.name in seen: + continue + filtered.insert(0, a) + seen.add(a.name) + + return filtered, base_attr_map + + +def _collect_base_attrs_broken(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + + N.B. *taken_attr_names* will be mutated. + + Adhere to the old incorrect behavior. + + Notably it collects from the front and considers inherited attributes which + leads to the buggy behavior reported in #428. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in cls.__mro__[1:-1]: + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) + taken_attr_names.add(a.name) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + return base_attrs, base_attr_map + + +def _transform_attrs( + cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer +): + """ + Transform all `_CountingAttr`s on a class into `Attribute`s. + + If *these* is passed, use that and don't look for them on the class. + + *collect_by_mro* is True, collect them in the correct MRO order, otherwise + use the old -- incorrect -- order. See #428. + + Return an `_Attributes`. + """ + cd = cls.__dict__ + anns = _get_annotations(cls) + + if these is not None: + ca_list = [(name, ca) for name, ca in iteritems(these)] + + if not isinstance(these, ordered_dict): + ca_list.sort(key=_counter_getter) + elif auto_attribs is True: + ca_names = { + name + for name, attr in cd.items() + if isinstance(attr, _CountingAttr) + } + ca_list = [] + annot_names = set() + for attr_name, type in anns.items(): + if _is_class_var(type): + continue + annot_names.add(attr_name) + a = cd.get(attr_name, NOTHING) + + if not isinstance(a, _CountingAttr): + if a is NOTHING: + a = attrib() + else: + a = attrib(default=a) + ca_list.append((attr_name, a)) + + unannotated = ca_names - annot_names + if len(unannotated) > 0: + raise UnannotatedAttributeError( + "The following `attr.ib`s lack a type annotation: " + + ", ".join( + sorted(unannotated, key=lambda n: cd.get(n).counter) + ) + + "." + ) + else: + ca_list = sorted( + ( + (name, attr) + for name, attr in cd.items() + if isinstance(attr, _CountingAttr) + ), + key=lambda e: e[1].counter, + ) + + own_attrs = [ + Attribute.from_counting_attr( + name=attr_name, ca=ca, type=anns.get(attr_name) + ) + for attr_name, ca in ca_list + ] + + if collect_by_mro: + base_attrs, base_attr_map = _collect_base_attrs( + cls, {a.name for a in own_attrs} + ) + else: + base_attrs, base_attr_map = _collect_base_attrs_broken( + cls, {a.name for a in own_attrs} + ) + + if kw_only: + own_attrs = [a.evolve(kw_only=True) for a in own_attrs] + base_attrs = [a.evolve(kw_only=True) for a in base_attrs] + + attrs = base_attrs + own_attrs + + # Mandatory vs non-mandatory attr order only matters when they are part of + # the __init__ signature and when they aren't kw_only (which are moved to + # the end and can be mandatory or non-mandatory in any order, as they will + # be specified as keyword args anyway). Check the order of those attrs: + had_default = False + for a in (a for a in attrs if a.init is not False and a.kw_only is False): + if had_default is True and a.default is NOTHING: + raise ValueError( + "No mandatory attributes allowed after an attribute with a " + "default value or factory. Attribute in question: %r" % (a,) + ) + + if had_default is False and a.default is not NOTHING: + had_default = True + + if field_transformer is not None: + attrs = field_transformer(cls, attrs) + + # Create AttrsClass *after* applying the field_transformer since it may + # add or remove attributes! + attr_names = [a.name for a in attrs] + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) + + +if PYPY: + + def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + ): + BaseException.__setattr__(self, name, value) + return + + raise FrozenInstanceError() + +else: + + def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + raise FrozenInstanceError() + + +def _frozen_delattrs(self, name): + """ + Attached to frozen classes as __delattr__. + """ + raise FrozenInstanceError() + + +class _ClassBuilder(object): + """ + Iteratively build *one* class. + """ + + __slots__ = ( + "_attr_names", + "_attrs", + "_base_attr_map", + "_base_names", + "_cache_hash", + "_cls", + "_cls_dict", + "_delete_attribs", + "_frozen", + "_has_pre_init", + "_has_post_init", + "_is_exc", + "_on_setattr", + "_slots", + "_weakref_slot", + "_wrote_own_setattr", + "_has_custom_setattr", + ) + + def __init__( + self, + cls, + these, + slots, + frozen, + weakref_slot, + getstate_setstate, + auto_attribs, + kw_only, + cache_hash, + is_exc, + collect_by_mro, + on_setattr, + has_custom_setattr, + field_transformer, + ): + attrs, base_attrs, base_map = _transform_attrs( + cls, + these, + auto_attribs, + kw_only, + collect_by_mro, + field_transformer, + ) + + self._cls = cls + self._cls_dict = dict(cls.__dict__) if slots else {} + self._attrs = attrs + self._base_names = set(a.name for a in base_attrs) + self._base_attr_map = base_map + self._attr_names = tuple(a.name for a in attrs) + self._slots = slots + self._frozen = frozen + self._weakref_slot = weakref_slot + self._cache_hash = cache_hash + self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) + self._delete_attribs = not bool(these) + self._is_exc = is_exc + self._on_setattr = on_setattr + + self._has_custom_setattr = has_custom_setattr + self._wrote_own_setattr = False + + self._cls_dict["__attrs_attrs__"] = self._attrs + + if frozen: + self._cls_dict["__setattr__"] = _frozen_setattrs + self._cls_dict["__delattr__"] = _frozen_delattrs + + self._wrote_own_setattr = True + elif on_setattr in ( + _ng_default_on_setattr, + setters.validate, + setters.convert, + ): + has_validator = has_converter = False + for a in attrs: + if a.validator is not None: + has_validator = True + if a.converter is not None: + has_converter = True + + if has_validator and has_converter: + break + if ( + ( + on_setattr == _ng_default_on_setattr + and not (has_validator or has_converter) + ) + or (on_setattr == setters.validate and not has_validator) + or (on_setattr == setters.convert and not has_converter) + ): + # If class-level on_setattr is set to convert + validate, but + # there's no field to convert or validate, pretend like there's + # no on_setattr. + self._on_setattr = None + + if getstate_setstate: + ( + self._cls_dict["__getstate__"], + self._cls_dict["__setstate__"], + ) = self._make_getstate_setstate() + + def __repr__(self): + return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() + else: + return self._patch_original_class() + + def _patch_original_class(self): + """ + Apply accumulated methods and return the class. + """ + cls = self._cls + base_names = self._base_names + + # Clean class of attribute definitions (`attr.ib()`s). + if self._delete_attribs: + for name in self._attr_names: + if ( + name not in base_names + and getattr(cls, name, _sentinel) is not _sentinel + ): + try: + delattr(cls, name) + except AttributeError: + # This can happen if a base class defines a class + # variable and we want to set an attribute with the + # same name by using only a type annotation. + pass + + # Attach our dunder methods. + for name, value in self._cls_dict.items(): + setattr(cls, name, value) + + # If we've inherited an attrs __setattr__ and don't write our own, + # reset it to object's. + if not self._wrote_own_setattr and getattr( + cls, "__attrs_own_setattr__", False + ): + cls.__attrs_own_setattr__ = False + + if not self._has_custom_setattr: + cls.__setattr__ = object.__setattr__ + + return cls + + def _create_slots_class(self): + """ + Build and return a new class with a `__slots__` attribute. + """ + cd = { + k: v + for k, v in iteritems(self._cls_dict) + if k not in tuple(self._attr_names) + ("__dict__", "__weakref__") + } + + # If our class doesn't have its own implementation of __setattr__ + # (either from the user or by us), check the bases, if one of them has + # an attrs-made __setattr__, that needs to be reset. We don't walk the + # MRO because we only care about our immediate base classes. + # XXX: This can be confused by subclassing a slotted attrs class with + # XXX: a non-attrs class and subclass the resulting class with an attrs + # XXX: class. See `test_slotted_confused` for details. For now that's + # XXX: OK with us. + if not self._wrote_own_setattr: + cd["__attrs_own_setattr__"] = False + + if not self._has_custom_setattr: + for base_cls in self._cls.__bases__: + if base_cls.__dict__.get("__attrs_own_setattr__", False): + cd["__setattr__"] = object.__setattr__ + break + + # Traverse the MRO to collect existing slots + # and check for an existing __weakref__. + existing_slots = dict() + weakref_inherited = False + for base_cls in self._cls.__mro__[1:-1]: + if base_cls.__dict__.get("__weakref__", None) is not None: + weakref_inherited = True + existing_slots.update( + { + name: getattr(base_cls, name) + for name in getattr(base_cls, "__slots__", []) + } + ) + + base_names = set(self._base_names) + + names = self._attr_names + if ( + self._weakref_slot + and "__weakref__" not in getattr(self._cls, "__slots__", ()) + and "__weakref__" not in names + and not weakref_inherited + ): + names += ("__weakref__",) + + # We only add the names of attributes that aren't inherited. + # Setting __slots__ to inherited attributes wastes memory. + slot_names = [name for name in names if name not in base_names] + # There are slots for attributes from current class + # that are defined in parent classes. + # As their descriptors may be overriden by a child class, + # we collect them here and update the class dict + reused_slots = { + slot: slot_descriptor + for slot, slot_descriptor in iteritems(existing_slots) + if slot in slot_names + } + slot_names = [name for name in slot_names if name not in reused_slots] + cd.update(reused_slots) + if self._cache_hash: + slot_names.append(_hash_cache_field) + cd["__slots__"] = tuple(slot_names) + + qualname = getattr(self._cls, "__qualname__", None) + if qualname is not None: + cd["__qualname__"] = qualname + + # Create new class based on old class and our methods. + cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) + + # The following is a fix for + # . On Python 3, + # if a method mentions `__class__` or uses the no-arg super(), the + # compiler will bake a reference to the class in the method itself + # as `method.__closure__`. Since we replace the class with a + # clone, we rewrite these references so it keeps working. + for item in cls.__dict__.values(): + if isinstance(item, (classmethod, staticmethod)): + # Class- and staticmethods hide their functions inside. + # These might need to be rewritten as well. + closure_cells = getattr(item.__func__, "__closure__", None) + elif isinstance(item, property): + # Workaround for property `super()` shortcut (PY3-only). + # There is no universal way for other descriptors. + closure_cells = getattr(item.fget, "__closure__", None) + else: + closure_cells = getattr(item, "__closure__", None) + + if not closure_cells: # Catch None or the empty list. + continue + for cell in closure_cells: + try: + match = cell.cell_contents is self._cls + except ValueError: # ValueError: Cell is empty + pass + else: + if match: + set_closure_cell(cell, cls) + + return cls + + def add_repr(self, ns): + self._cls_dict["__repr__"] = self._add_method_dunders( + _make_repr(self._attrs, ns, self._cls) + ) + return self + + def add_str(self): + repr = self._cls_dict.get("__repr__") + if repr is None: + raise ValueError( + "__str__ can only be generated if a __repr__ exists." + ) + + def __str__(self): + return self.__repr__() + + self._cls_dict["__str__"] = self._add_method_dunders(__str__) + return self + + def _make_getstate_setstate(self): + """ + Create custom __setstate__ and __getstate__ methods. + """ + # __weakref__ is not writable. + state_attr_names = tuple( + an for an in self._attr_names if an != "__weakref__" + ) + + def slots_getstate(self): + """ + Automatically created by attrs. + """ + return tuple(getattr(self, name) for name in state_attr_names) + + hash_caching_enabled = self._cache_hash + + def slots_setstate(self, state): + """ + Automatically created by attrs. + """ + __bound_setattr = _obj_setattr.__get__(self, Attribute) + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + + # The hash code cache is not included when the object is + # serialized, but it still needs to be initialized to None to + # indicate that the first call to __hash__ should be a cache + # miss. + if hash_caching_enabled: + __bound_setattr(_hash_cache_field, None) + + return slots_getstate, slots_setstate + + def make_unhashable(self): + self._cls_dict["__hash__"] = None + return self + + def add_hash(self): + self._cls_dict["__hash__"] = self._add_method_dunders( + _make_hash( + self._cls, + self._attrs, + frozen=self._frozen, + cache_hash=self._cache_hash, + ) + ) + + return self + + def add_init(self): + self._cls_dict["__init__"] = self._add_method_dunders( + _make_init( + self._cls, + self._attrs, + self._has_pre_init, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=False, + ) + ) + + return self + + def add_match_args(self): + self._cls_dict["__match_args__"] = tuple( + field.name + for field in self._attrs + if field.init and not field.kw_only + ) + + def add_attrs_init(self): + self._cls_dict["__attrs_init__"] = self._add_method_dunders( + _make_init( + self._cls, + self._attrs, + self._has_pre_init, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=True, + ) + ) + + return self + + def add_eq(self): + cd = self._cls_dict + + cd["__eq__"] = self._add_method_dunders( + _make_eq(self._cls, self._attrs) + ) + cd["__ne__"] = self._add_method_dunders(_make_ne()) + + return self + + def add_order(self): + cd = self._cls_dict + + cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( + self._add_method_dunders(meth) + for meth in _make_order(self._cls, self._attrs) + ) + + return self + + def add_setattr(self): + if self._frozen: + return self + + sa_attrs = {} + for a in self._attrs: + on_setattr = a.on_setattr or self._on_setattr + if on_setattr and on_setattr is not setters.NO_OP: + sa_attrs[a.name] = a, on_setattr + + if not sa_attrs: + return self + + if self._has_custom_setattr: + # We need to write a __setattr__ but there already is one! + raise ValueError( + "Can't combine custom __setattr__ with on_setattr hooks." + ) + + # docstring comes from _add_method_dunders + def __setattr__(self, name, val): + try: + a, hook = sa_attrs[name] + except KeyError: + nval = val + else: + nval = hook(self, a, val) + + _obj_setattr(self, name, nval) + + self._cls_dict["__attrs_own_setattr__"] = True + self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) + self._wrote_own_setattr = True + + return self + + def _add_method_dunders(self, method): + """ + Add __module__ and __qualname__ to a *method* if possible. + """ + try: + method.__module__ = self._cls.__module__ + except AttributeError: + pass + + try: + method.__qualname__ = ".".join( + (self._cls.__qualname__, method.__name__) + ) + except AttributeError: + pass + + try: + method.__doc__ = "Method generated by attrs for class %s." % ( + self._cls.__qualname__, + ) + except AttributeError: + pass + + return method + + +_CMP_DEPRECATION = ( + "The usage of `cmp` is deprecated and will be removed on or after " + "2021-06-01. Please use `eq` and `order` instead." +) + + +def _determine_attrs_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + raise ValueError("Don't mix `cmp` with `eq' and `order`.") + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + return cmp, cmp + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq = default_eq + + if order is None: + order = eq + + if eq is False and order is True: + raise ValueError("`order` can only be True if `eq` is True too.") + + return eq, order + + +def _determine_attrib_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + raise ValueError("Don't mix `cmp` with `eq' and `order`.") + + def decide_callable_or_boolean(value): + """ + Decide whether a key function is used. + """ + if callable(value): + value, key = True, value + else: + key = None + return value, key + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + cmp, cmp_key = decide_callable_or_boolean(cmp) + return cmp, cmp_key, cmp, cmp_key + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq, eq_key = default_eq, None + else: + eq, eq_key = decide_callable_or_boolean(eq) + + if order is None: + order, order_key = eq, eq_key + else: + order, order_key = decide_callable_or_boolean(order) + + if eq is False and order is True: + raise ValueError("`order` can only be True if `eq` is True too.") + + return eq, eq_key, order, order_key + + +def _determine_whether_to_implement( + cls, flag, auto_detect, dunders, default=True +): + """ + Check whether we should implement a set of methods for *cls*. + + *flag* is the argument passed into @attr.s like 'init', *auto_detect* the + same as passed into @attr.s and *dunders* is a tuple of attribute names + whose presence signal that the user has implemented it themselves. + + Return *default* if no reason for either for or against is found. + + auto_detect must be False on Python 2. + """ + if flag is True or flag is False: + return flag + + if flag is None and auto_detect is False: + return default + + # Logically, flag is None and auto_detect is True here. + for dunder in dunders: + if _has_own_attribute(cls, dunder): + return False + + return default + + +def attrs( + maybe_cls=None, + these=None, + repr_ns=None, + repr=None, + cmp=None, + hash=None, + init=None, + slots=False, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=False, + kw_only=False, + cache_hash=False, + auto_exc=False, + eq=None, + order=None, + auto_detect=False, + collect_by_mro=False, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, +): + r""" + A class decorator that adds `dunder + `_\ -methods according to the + specified attributes using `attr.ib` or the *these* argument. + + :param these: A dictionary of name to `attr.ib` mappings. This is + useful to avoid the definition of your attributes within the class body + because you can't (e.g. if you want to add ``__repr__`` methods to + Django models) or don't want to. + + If *these* is not ``None``, ``attrs`` will *not* search the class body + for attributes and will *not* remove any attributes from it. + + If *these* is an ordered dict (`dict` on Python 3.6+, + `collections.OrderedDict` otherwise), the order is deduced from + the order of the attributes inside *these*. Otherwise the order + of the definition of the attributes is used. + + :type these: `dict` of `str` to `attr.ib` + + :param str repr_ns: When using nested classes, there's no way in Python 2 + to automatically detect that. Therefore it's possible to set the + namespace explicitly for a more meaningful ``repr`` output. + :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*, + *order*, and *hash* arguments explicitly, assume they are set to + ``True`` **unless any** of the involved methods for one of the + arguments is implemented in the *current* class (i.e. it is *not* + inherited from some base class). + + So for example by implementing ``__eq__`` on a class yourself, + ``attrs`` will deduce ``eq=False`` and will create *neither* + ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible + ``__ne__`` by default, so it *should* be enough to only implement + ``__eq__`` in most cases). + + .. warning:: + + If you prevent ``attrs`` from creating the ordering methods for you + (``order=False``, e.g. by implementing ``__le__``), it becomes + *your* responsibility to make sure its ordering is sound. The best + way is to use the `functools.total_ordering` decorator. + + + Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, + *cmp*, or *hash* overrides whatever *auto_detect* would determine. + + *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises + an `attrs.exceptions.PythonTooOldError`. + + :param bool repr: Create a ``__repr__`` method with a human readable + representation of ``attrs`` attributes.. + :param bool str: Create a ``__str__`` method that is identical to + ``__repr__``. This is usually not necessary except for + `Exception`\ s. + :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` + and ``__ne__`` methods that check two instances for equality. + + They compare the instances as if they were tuples of their ``attrs`` + attributes if and only if the types of both classes are *identical*! + :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, + ``__gt__``, and ``__ge__`` methods that behave like *eq* above and + allow instances to be ordered. If ``None`` (default) mirror value of + *eq*. + :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq* + and *order* to the same value. Must not be mixed with *eq* or *order*. + :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method + is generated according how *eq* and *frozen* are set. + + 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. + 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to + None, marking it unhashable (which it is). + 3. If *eq* is False, ``__hash__`` will be left untouched meaning the + ``__hash__`` method of the base class will be used (if base class is + ``object``, this means it will fall back to id-based hashing.). + + Although not recommended, you can decide for yourself and force + ``attrs`` to create one (e.g. if the class is immutable even though you + didn't freeze it programmatically) by passing ``True`` or not. Both of + these cases are rather special and should be used carefully. + + See our documentation on `hashing`, Python's documentation on + `object.__hash__`, and the `GitHub issue that led to the default \ + behavior `_ for more + details. + :param bool init: Create a ``__init__`` method that initializes the + ``attrs`` attributes. Leading underscores are stripped for the argument + name. If a ``__attrs_pre_init__`` method exists on the class, it will + be called before the class is initialized. If a ``__attrs_post_init__`` + method exists on the class, it will be called after the class is fully + initialized. + + If ``init`` is ``False``, an ``__attrs_init__`` method will be + injected instead. This allows you to define a custom ``__init__`` + method that can do pre-init work such as ``super().__init__()``, + and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. + :param bool slots: Create a `slotted class ` that's more + memory-efficient. Slotted classes are generally superior to the default + dict classes, but have some gotchas you should know about, so we + encourage you to read the `glossary entry `. + :param bool frozen: Make instances immutable after initialization. If + someone attempts to modify a frozen instance, + `attr.exceptions.FrozenInstanceError` is raised. + + .. note:: + + 1. This is achieved by installing a custom ``__setattr__`` method + on your class, so you can't implement your own. + + 2. True immutability is impossible in Python. + + 3. This *does* have a minor a runtime performance `impact + ` when initializing new instances. In other words: + ``__init__`` is slightly slower with ``frozen=True``. + + 4. If a class is frozen, you cannot modify ``self`` in + ``__attrs_post_init__`` or a self-written ``__init__``. You can + circumvent that limitation by using + ``object.__setattr__(self, "attribute_name", value)``. + + 5. Subclasses of a frozen class are frozen too. + + :param bool weakref_slot: Make instances weak-referenceable. This has no + effect unless ``slots`` is also enabled. + :param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated + attributes (Python 3.6 and later only) from the class body. + + In this case, you **must** annotate every field. If ``attrs`` + encounters a field that is set to an `attr.ib` but lacks a type + annotation, an `attr.exceptions.UnannotatedAttributeError` is + raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't + want to set a type. + + If you assign a value to those attributes (e.g. ``x: int = 42``), that + value becomes the default value like if it were passed using + ``attr.ib(default=42)``. Passing an instance of `attrs.Factory` also + works as expected in most cases (see warning below). + + Attributes annotated as `typing.ClassVar`, and attributes that are + neither annotated nor set to an `attr.ib` are **ignored**. + + .. warning:: + For features that use the attribute name to create decorators (e.g. + `validators `), you still *must* assign `attr.ib` to + them. Otherwise Python will either not find the name or try to use + the default value to call e.g. ``validator`` on it. + + These errors can be quite confusing and probably the most common bug + report on our bug tracker. + + .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/ + :param bool kw_only: Make all attributes keyword-only (Python 3+) + in the generated ``__init__`` (if ``init`` is ``False``, this + parameter is ignored). + :param bool cache_hash: Ensure that the object's hash code is computed + only once and stored on the object. If this is set to ``True``, + hashing must be either explicitly or implicitly enabled for this + class. If the hash code is cached, avoid any reassignments of + fields involved in hash code computation or mutations of the objects + those fields point to after object creation. If such changes occur, + the behavior of the object's hash code is undefined. + :param bool auto_exc: If the class subclasses `BaseException` + (which implicitly includes any subclass of any exception), the + following happens to behave like a well-behaved Python exceptions + class: + + - the values for *eq*, *order*, and *hash* are ignored and the + instances compare and hash by the instance's ids (N.B. ``attrs`` will + *not* remove existing implementations of ``__hash__`` or the equality + methods. It just won't add own ones.), + - all attributes that are either passed into ``__init__`` or have a + default value are additionally available as a tuple in the ``args`` + attribute, + - the value of *str* is ignored leaving ``__str__`` to base classes. + :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` + collects attributes from base classes. The default behavior is + incorrect in certain cases of multiple inheritance. It should be on by + default but is kept off for backward-compatibility. + + See issue `#428 `_ for + more details. + + :param Optional[bool] getstate_setstate: + .. note:: + This is usually only interesting for slotted classes and you should + probably just set *auto_detect* to `True`. + + If `True`, ``__getstate__`` and + ``__setstate__`` are generated and attached to the class. This is + necessary for slotted classes to be pickleable. If left `None`, it's + `True` by default for slotted classes and ``False`` for dict classes. + + If *auto_detect* is `True`, and *getstate_setstate* is left `None`, + and **either** ``__getstate__`` or ``__setstate__`` is detected directly + on the class (i.e. not inherited), it is set to `False` (this is usually + what you want). + + :param on_setattr: A callable that is run whenever the user attempts to set + an attribute (either by assignment like ``i.x = 42`` or by using + `setattr` like ``setattr(i, "x", 42)``). It receives the same arguments + as validators: the instance, the attribute that is being modified, and + the new value. + + If no exception is raised, the attribute is set to the return value of + the callable. + + If a list of callables is passed, they're automatically wrapped in an + `attrs.setters.pipe`. + + :param Optional[callable] field_transformer: + A function that is called with the original class object and all + fields right before ``attrs`` finalizes the class. You can use + this, e.g., to automatically add converters or validators to + fields based on their types. See `transform-fields` for more details. + + :param bool match_args: + If `True` (default), set ``__match_args__`` on the class to support + `PEP 634 `_ (Structural + Pattern Matching). It is a tuple of all positional-only ``__init__`` + parameter names on Python 3.10 and later. Ignored on older Python + versions. + + .. versionadded:: 16.0.0 *slots* + .. versionadded:: 16.1.0 *frozen* + .. versionadded:: 16.3.0 *str* + .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. + .. versionchanged:: 17.1.0 + *hash* supports ``None`` as value which is also the default now. + .. versionadded:: 17.3.0 *auto_attribs* + .. versionchanged:: 18.1.0 + If *these* is passed, no attributes are deleted from the class body. + .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. + .. versionadded:: 18.2.0 *weakref_slot* + .. deprecated:: 18.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a + `DeprecationWarning` if the classes compared are subclasses of + each other. ``__eq`` and ``__ne__`` never tried to compared subclasses + to each other. + .. versionchanged:: 19.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider + subclasses comparable anymore. + .. versionadded:: 18.2.0 *kw_only* + .. versionadded:: 18.2.0 *cache_hash* + .. versionadded:: 19.1.0 *auto_exc* + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *auto_detect* + .. versionadded:: 20.1.0 *collect_by_mro* + .. versionadded:: 20.1.0 *getstate_setstate* + .. versionadded:: 20.1.0 *on_setattr* + .. versionadded:: 20.3.0 *field_transformer* + .. versionchanged:: 21.1.0 + ``init=False`` injects ``__attrs_init__`` + .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 21.3.0 *match_args* + """ + if auto_detect and PY2: + raise PythonTooOldError( + "auto_detect only works on Python 3 and later." + ) + + eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) + hash_ = hash # work around the lack of nonlocal + + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + def wrap(cls): + + if getattr(cls, "__class__", None) is None: + raise TypeError("attrs only works with new-style classes.") + + is_frozen = frozen or _has_frozen_base_class(cls) + is_exc = auto_exc is True and issubclass(cls, BaseException) + has_own_setattr = auto_detect and _has_own_attribute( + cls, "__setattr__" + ) + + if has_own_setattr and is_frozen: + raise ValueError("Can't freeze a class with a custom __setattr__.") + + builder = _ClassBuilder( + cls, + these, + slots, + is_frozen, + weakref_slot, + _determine_whether_to_implement( + cls, + getstate_setstate, + auto_detect, + ("__getstate__", "__setstate__"), + default=slots, + ), + auto_attribs, + kw_only, + cache_hash, + is_exc, + collect_by_mro, + on_setattr, + has_own_setattr, + field_transformer, + ) + if _determine_whether_to_implement( + cls, repr, auto_detect, ("__repr__",) + ): + builder.add_repr(repr_ns) + if str is True: + builder.add_str() + + eq = _determine_whether_to_implement( + cls, eq_, auto_detect, ("__eq__", "__ne__") + ) + if not is_exc and eq is True: + builder.add_eq() + if not is_exc and _determine_whether_to_implement( + cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__") + ): + builder.add_order() + + builder.add_setattr() + + if ( + hash_ is None + and auto_detect is True + and _has_own_attribute(cls, "__hash__") + ): + hash = False + else: + hash = hash_ + if hash is not True and hash is not False and hash is not None: + # Can't use `hash in` because 1 == True for example. + raise TypeError( + "Invalid value for hash. Must be True, False, or None." + ) + elif hash is False or (hash is None and eq is False) or is_exc: + # Don't do anything. Should fall back to __object__'s __hash__ + # which is by id. + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " hashing must be either explicitly or implicitly " + "enabled." + ) + elif hash is True or ( + hash is None and eq is True and is_frozen is True + ): + # Build a __hash__ if told so, or if it's safe. + builder.add_hash() + else: + # Raise TypeError on attempts to hash. + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " hashing must be either explicitly or implicitly " + "enabled." + ) + builder.make_unhashable() + + if _determine_whether_to_implement( + cls, init, auto_detect, ("__init__",) + ): + builder.add_init() + else: + builder.add_attrs_init() + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " init must be True." + ) + + if ( + PY310 + and match_args + and not _has_own_attribute(cls, "__match_args__") + ): + builder.add_match_args() + + return builder.build_class() + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but ``None`` if used as `@attrs()`. + if maybe_cls is None: + return wrap + else: + return wrap(maybe_cls) + + +_attrs = attrs +""" +Internal alias so we can use it in functions that take an argument called +*attrs*. +""" + + +if PY2: + + def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return ( + getattr(cls.__setattr__, "__module__", None) + == _frozen_setattrs.__module__ + and cls.__setattr__.__name__ == _frozen_setattrs.__name__ + ) + +else: + + def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return cls.__setattr__ == _frozen_setattrs + + +def _generate_unique_filename(cls, func_name): + """ + Create a "filename" suitable for a function being generated. + """ + unique_filename = "".format( + func_name, + cls.__module__, + getattr(cls, "__qualname__", cls.__name__), + ) + return unique_filename + + +def _make_hash(cls, attrs, frozen, cache_hash): + attrs = tuple( + a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) + ) + + tab = " " + + unique_filename = _generate_unique_filename(cls, "hash") + type_hash = hash(unique_filename) + + hash_def = "def __hash__(self" + hash_func = "hash((" + closing_braces = "))" + if not cache_hash: + hash_def += "):" + else: + if not PY2: + hash_def += ", *" + + hash_def += ( + ", _cache_wrapper=" + + "__import__('attr._make')._make._CacheHashWrapper):" + ) + hash_func = "_cache_wrapper(" + hash_func + closing_braces += ")" + + method_lines = [hash_def] + + def append_hash_computation_lines(prefix, indent): + """ + Generate the code for actually computing the hash code. + Below this will either be returned directly or used to compute + a value which is then cached, depending on the value of cache_hash + """ + + method_lines.extend( + [ + indent + prefix + hash_func, + indent + " %d," % (type_hash,), + ] + ) + + for a in attrs: + method_lines.append(indent + " self.%s," % a.name) + + method_lines.append(indent + " " + closing_braces) + + if cache_hash: + method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) + if frozen: + append_hash_computation_lines( + "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 + ) + method_lines.append(tab * 2 + ")") # close __setattr__ + else: + append_hash_computation_lines( + "self.%s = " % _hash_cache_field, tab * 2 + ) + method_lines.append(tab + "return self.%s" % _hash_cache_field) + else: + append_hash_computation_lines("return ", tab) + + script = "\n".join(method_lines) + return _make_method("__hash__", script, unique_filename) + + +def _add_hash(cls, attrs): + """ + Add a hash method to *cls*. + """ + cls.__hash__ = _make_hash(cls, attrs, frozen=False, cache_hash=False) + return cls + + +def _make_ne(): + """ + Create __ne__ method. + """ + + def __ne__(self, other): + """ + Check equality and either forward a NotImplemented or + return the result negated. + """ + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + + return not result + + return __ne__ + + +def _make_eq(cls, attrs): + """ + Create __eq__ method for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.eq] + + unique_filename = _generate_unique_filename(cls, "eq") + lines = [ + "def __eq__(self, other):", + " if other.__class__ is not self.__class__:", + " return NotImplemented", + ] + + # We can't just do a big self.x = other.x and... clause due to + # irregularities like nan == nan is false but (nan,) == (nan,) is true. + globs = {} + if attrs: + lines.append(" return (") + others = [" ) == ("] + for a in attrs: + if a.eq_key: + cmp_name = "_%s_key" % (a.name,) + # Add the key function to the global namespace + # of the evaluated function. + globs[cmp_name] = a.eq_key + lines.append( + " %s(self.%s)," + % ( + cmp_name, + a.name, + ) + ) + others.append( + " %s(other.%s)," + % ( + cmp_name, + a.name, + ) + ) + else: + lines.append(" self.%s," % (a.name,)) + others.append(" other.%s," % (a.name,)) + + lines += others + [" )"] + else: + lines.append(" return True") + + script = "\n".join(lines) + + return _make_method("__eq__", script, unique_filename, globs) + + +def _make_order(cls, attrs): + """ + Create ordering methods for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.order] + + def attrs_to_tuple(obj): + """ + Save us some typing. + """ + return tuple( + key(value) if key else value + for value, key in ( + (getattr(obj, a.name), a.order_key) for a in attrs + ) + ) + + def __lt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) < attrs_to_tuple(other) + + return NotImplemented + + def __le__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) <= attrs_to_tuple(other) + + return NotImplemented + + def __gt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) > attrs_to_tuple(other) + + return NotImplemented + + def __ge__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) >= attrs_to_tuple(other) + + return NotImplemented + + return __lt__, __le__, __gt__, __ge__ + + +def _add_eq(cls, attrs=None): + """ + Add equality methods to *cls* with *attrs*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + cls.__eq__ = _make_eq(cls, attrs) + cls.__ne__ = _make_ne() + + return cls + + +if HAS_F_STRINGS: + + def _make_repr(attrs, ns, cls): + unique_filename = _generate_unique_filename(cls, "repr") + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r + for name, r, _ in attr_names_with_reprs + if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name + if i + else 'getattr(self, "' + name + '", NOTHING)' + ) + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) + ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) + + if ns is None: + cls_name_fragment = ( + '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + ) + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" + + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), + " finally:", + " already_repring.remove(id(self))", + ] + + return _make_method( + "__repr__", "\n".join(lines), unique_filename, globs=globs + ) + +else: + + def _make_repr(attrs, ns, _): + """ + Make a repr method that includes relevant *attrs*, adding *ns* to the + full name. + """ + + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, repr if a.repr is True else a.repr) + for a in attrs + if a.repr is not False + ) + + def __repr__(self): + """ + Automatically created by attrs. + """ + try: + already_repring = _compat.repr_context.already_repring + except AttributeError: + already_repring = set() + _compat.repr_context.already_repring = already_repring + + if id(self) in already_repring: + return "..." + real_cls = self.__class__ + if ns is None: + qualname = getattr(real_cls, "__qualname__", None) + if qualname is not None: # pragma: no cover + # This case only happens on Python 3.5 and 3.6. We exclude + # it from coverage, because we don't want to slow down our + # test suite by running them under coverage too for this + # one line. + class_name = qualname.rsplit(">.", 1)[-1] + else: + class_name = real_cls.__name__ + else: + class_name = ns + "." + real_cls.__name__ + + # Since 'self' remains on the stack (i.e.: strongly referenced) + # for the duration of this call, it's safe to depend on id(...) + # stability, and not need to track the instance and therefore + # worry about properties like weakref- or hash-ability. + already_repring.add(id(self)) + try: + result = [class_name, "("] + first = True + for name, attr_repr in attr_names_with_reprs: + if first: + first = False + else: + result.append(", ") + result.extend( + (name, "=", attr_repr(getattr(self, name, NOTHING))) + ) + return "".join(result) + ")" + finally: + already_repring.remove(id(self)) + + return __repr__ + + +def _add_repr(cls, ns=None, attrs=None): + """ + Add a repr method to *cls*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + cls.__repr__ = _make_repr(attrs, ns, cls) + return cls + + +def fields(cls): + """ + Return the tuple of ``attrs`` attributes for a class. + + The tuple also allows accessing the fields by their names (see below for + examples). + + :param type cls: Class to introspect. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + :rtype: tuple (with name accessors) of `attrs.Attribute` + + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + """ + if not isclass(cls): + raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + raise NotAnAttrsClassError( + "{cls!r} is not an attrs-decorated class.".format(cls=cls) + ) + return attrs + + +def fields_dict(cls): + """ + Return an ordered dictionary of ``attrs`` attributes for a class, whose + keys are the attribute names. + + :param type cls: Class to introspect. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + :rtype: an ordered dict where keys are attribute names and values are + `attrs.Attribute`\\ s. This will be a `dict` if it's + naturally ordered like on Python 3.6+ or an + :class:`~collections.OrderedDict` otherwise. + + .. versionadded:: 18.1.0 + """ + if not isclass(cls): + raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + raise NotAnAttrsClassError( + "{cls!r} is not an attrs-decorated class.".format(cls=cls) + ) + return ordered_dict(((a.name, a) for a in attrs)) + + +def validate(inst): + """ + Validate all attributes on *inst* that have a validator. + + Leaves all exceptions through. + + :param inst: Instance of a class with ``attrs`` attributes. + """ + if _config._run_validators is False: + return + + for a in fields(inst.__class__): + v = a.validator + if v is not None: + v(inst, a, getattr(inst, a.name)) + + +def _is_slot_cls(cls): + return "__slots__" in cls.__dict__ + + +def _is_slot_attr(a_name, base_attr_map): + """ + Check if the attribute name comes from a slot class. + """ + return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name]) + + +def _make_init( + cls, + attrs, + pre_init, + post_init, + frozen, + slots, + cache_hash, + base_attr_map, + is_exc, + cls_on_setattr, + attrs_init, +): + has_cls_on_setattr = ( + cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP + ) + + if frozen and has_cls_on_setattr: + raise ValueError("Frozen classes can't use on_setattr.") + + needs_cached_setattr = cache_hash or frozen + filtered_attrs = [] + attr_dict = {} + for a in attrs: + if not a.init and a.default is NOTHING: + continue + + filtered_attrs.append(a) + attr_dict[a.name] = a + + if a.on_setattr is not None: + if frozen is True: + raise ValueError("Frozen classes can't use on_setattr.") + + needs_cached_setattr = True + elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: + needs_cached_setattr = True + + unique_filename = _generate_unique_filename(cls, "init") + + script, globs, annotations = _attrs_to_init_script( + filtered_attrs, + frozen, + slots, + pre_init, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_cls_on_setattr, + attrs_init, + ) + if cls.__module__ in sys.modules: + # This makes typing.get_type_hints(CLS.__init__) resolve string types. + globs.update(sys.modules[cls.__module__].__dict__) + + globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) + + if needs_cached_setattr: + # Save the lookup overhead in __init__ if we need to circumvent + # setattr hooks. + globs["_cached_setattr"] = _obj_setattr + + init = _make_method( + "__attrs_init__" if attrs_init else "__init__", + script, + unique_filename, + globs, + ) + init.__annotations__ = annotations + + return init + + +def _setattr(attr_name, value_var, has_on_setattr): + """ + Use the cached object.setattr to set *attr_name* to *value_var*. + """ + return "_setattr('%s', %s)" % (attr_name, value_var) + + +def _setattr_with_converter(attr_name, value_var, has_on_setattr): + """ + Use the cached object.setattr to set *attr_name* to *value_var*, but run + its converter first. + """ + return "_setattr('%s', %s(%s))" % ( + attr_name, + _init_converter_pat % (attr_name,), + value_var, + ) + + +def _assign(attr_name, value, has_on_setattr): + """ + Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise + relegate to _setattr. + """ + if has_on_setattr: + return _setattr(attr_name, value, True) + + return "self.%s = %s" % (attr_name, value) + + +def _assign_with_converter(attr_name, value_var, has_on_setattr): + """ + Unless *attr_name* has an on_setattr hook, use normal assignment after + conversion. Otherwise relegate to _setattr_with_converter. + """ + if has_on_setattr: + return _setattr_with_converter(attr_name, value_var, True) + + return "self.%s = %s(%s)" % ( + attr_name, + _init_converter_pat % (attr_name,), + value_var, + ) + + +if PY2: + + def _unpack_kw_only_py2(attr_name, default=None): + """ + Unpack *attr_name* from _kw_only dict. + """ + if default is not None: + arg_default = ", %s" % default + else: + arg_default = "" + return "%s = _kw_only.pop('%s'%s)" % ( + attr_name, + attr_name, + arg_default, + ) + + def _unpack_kw_only_lines_py2(kw_only_args): + """ + Unpack all *kw_only_args* from _kw_only dict and handle errors. + + Given a list of strings "{attr_name}" and "{attr_name}={default}" + generates list of lines of code that pop attrs from _kw_only dict and + raise TypeError similar to builtin if required attr is missing or + extra key is passed. + + >>> print("\n".join(_unpack_kw_only_lines_py2(["a", "b=42"]))) + try: + a = _kw_only.pop('a') + b = _kw_only.pop('b', 42) + except KeyError as _key_error: + raise TypeError( + ... + if _kw_only: + raise TypeError( + ... + """ + lines = ["try:"] + lines.extend( + " " + _unpack_kw_only_py2(*arg.split("=")) + for arg in kw_only_args + ) + lines += """\ +except KeyError as _key_error: + raise TypeError( + '__init__() missing required keyword-only argument: %s' % _key_error + ) +if _kw_only: + raise TypeError( + '__init__() got an unexpected keyword argument %r' + % next(iter(_kw_only)) + ) +""".split( + "\n" + ) + return lines + + +def _attrs_to_init_script( + attrs, + frozen, + slots, + pre_init, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_cls_on_setattr, + attrs_init, +): + """ + Return a script of an initializer for *attrs* and a dict of globals. + + The globals are expected by the generated script. + + If *frozen* is True, we cannot set the attributes directly so we use + a cached ``object.__setattr__``. + """ + lines = [] + if pre_init: + lines.append("self.__attrs_pre_init__()") + + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. + # Note _setattr will be used again below if cache_hash is True + "_setattr = _cached_setattr.__get__(self, self.__class__)" + ) + + if frozen is True: + if slots is True: + fmt_setter = _setattr + fmt_setter_with_converter = _setattr_with_converter + else: + # Dict frozen classes assign directly to __dict__. + # But only if the attribute doesn't come from an ancestor slot + # class. + # Note _inst_dict will be used again below if cache_hash is True + lines.append("_inst_dict = self.__dict__") + + def fmt_setter(attr_name, value_var, has_on_setattr): + if _is_slot_attr(attr_name, base_attr_map): + return _setattr(attr_name, value_var, has_on_setattr) + + return "_inst_dict['%s'] = %s" % (attr_name, value_var) + + def fmt_setter_with_converter( + attr_name, value_var, has_on_setattr + ): + if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): + return _setattr_with_converter( + attr_name, value_var, has_on_setattr + ) + + return "_inst_dict['%s'] = %s(%s)" % ( + attr_name, + _init_converter_pat % (attr_name,), + value_var, + ) + + else: + # Not frozen. + fmt_setter = _assign + fmt_setter_with_converter = _assign_with_converter + + args = [] + kw_only_args = [] + attrs_to_validate = [] + + # This is a dictionary of names to validator and converter callables. + # Injecting this into __init__ globals lets us avoid lookups. + names_for_globals = {} + annotations = {"return": None} + + for a in attrs: + if a.validator: + attrs_to_validate.append(a) + + attr_name = a.name + has_on_setattr = a.on_setattr is not None or ( + a.on_setattr is not setters.NO_OP and has_cls_on_setattr + ) + arg_name = a.name.lstrip("_") + + has_factory = isinstance(a.default, Factory) + if has_factory and a.default.takes_self: + maybe_self = "self" + else: + maybe_self = "" + + if a.init is False: + if has_factory: + init_factory_name = _init_factory_pat.format(a.name) + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + init_factory_name + "(%s)" % (maybe_self,), + has_on_setattr, + ) + ) + conv_name = _init_converter_pat % (a.name,) + names_for_globals[conv_name] = a.converter + else: + lines.append( + fmt_setter( + attr_name, + init_factory_name + "(%s)" % (maybe_self,), + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + "attr_dict['%s'].default" % (attr_name,), + has_on_setattr, + ) + ) + conv_name = _init_converter_pat % (a.name,) + names_for_globals[conv_name] = a.converter + else: + lines.append( + fmt_setter( + attr_name, + "attr_dict['%s'].default" % (attr_name,), + has_on_setattr, + ) + ) + elif a.default is not NOTHING and not has_factory: + arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr + ) + ) + names_for_globals[ + _init_converter_pat % (a.name,) + ] = a.converter + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + elif has_factory: + arg = "%s=NOTHING" % (arg_name,) + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + lines.append("if %s is not NOTHING:" % (arg_name,)) + + init_factory_name = _init_factory_pat.format(a.name) + if a.converter is not None: + lines.append( + " " + + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr + ) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter_with_converter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[ + _init_converter_pat % (a.name,) + ] = a.converter + else: + lines.append( + " " + fmt_setter(attr_name, arg_name, has_on_setattr) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.kw_only: + kw_only_args.append(arg_name) + else: + args.append(arg_name) + + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr + ) + ) + names_for_globals[ + _init_converter_pat % (a.name,) + ] = a.converter + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + if a.init is True: + if a.type is not None and a.converter is None: + annotations[arg_name] = a.type + elif a.converter is not None and not PY2: + # Try to get the type from the converter. + sig = None + try: + sig = inspect.signature(a.converter) + except (ValueError, TypeError): # inspect failed + pass + if sig: + sig_params = list(sig.parameters.values()) + if ( + sig_params + and sig_params[0].annotation + is not inspect.Parameter.empty + ): + annotations[arg_name] = sig_params[0].annotation + + if attrs_to_validate: # we can skip this if there are no validators. + names_for_globals["_config"] = _config + lines.append("if _config._run_validators is True:") + for a in attrs_to_validate: + val_name = "__attr_validator_" + a.name + attr_name = "__attr_" + a.name + lines.append( + " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) + ) + names_for_globals[val_name] = a.validator + names_for_globals[attr_name] = a + + if post_init: + lines.append("self.__attrs_post_init__()") + + # because this is set only after __attrs_post_init is called, a crash + # will result if post-init tries to access the hash code. This seemed + # preferable to setting this beforehand, in which case alteration to + # field values during post-init combined with post-init accessing the + # hash code would result in silent bugs. + if cache_hash: + if frozen: + if slots: + # if frozen and slots, then _setattr defined above + init_hash_cache = "_setattr('%s', %s)" + else: + # if frozen and not slots, then _inst_dict defined above + init_hash_cache = "_inst_dict['%s'] = %s" + else: + init_hash_cache = "self.%s = %s" + lines.append(init_hash_cache % (_hash_cache_field, "None")) + + # For exceptions we rely on BaseException.__init__ for proper + # initialization. + if is_exc: + vals = ",".join("self." + a.name for a in attrs if a.init) + + lines.append("BaseException.__init__(self, %s)" % (vals,)) + + args = ", ".join(args) + if kw_only_args: + if PY2: + lines = _unpack_kw_only_lines_py2(kw_only_args) + lines + + args += "%s**_kw_only" % (", " if args else "",) # leading comma + else: + args += "%s*, %s" % ( + ", " if args else "", # leading comma + ", ".join(kw_only_args), # kw_only args + ) + return ( + """\ +def {init_name}(self, {args}): + {lines} +""".format( + init_name=("__attrs_init__" if attrs_init else "__init__"), + args=args, + lines="\n ".join(lines) if lines else "pass", + ), + names_for_globals, + annotations, + ) + + +class Attribute(object): + """ + *Read-only* representation of an attribute. + + The class has *all* arguments of `attr.ib` (except for ``factory`` + which is only syntactic sugar for ``default=Factory(...)`` plus the + following: + + - ``name`` (`str`): The name of the attribute. + - ``inherited`` (`bool`): Whether or not that attribute has been inherited + from a base class. + - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables + that are used for comparing and ordering objects by this attribute, + respectively. These are set by passing a callable to `attr.ib`'s ``eq``, + ``order``, or ``cmp`` arguments. See also :ref:`comparison customization + `. + + Instances of this class are frequently used for introspection purposes + like: + + - `fields` returns a tuple of them. + - Validators get them passed as the first argument. + - The :ref:`field transformer ` hook receives a list of + them. + + .. versionadded:: 20.1.0 *inherited* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.2.0 *inherited* is not taken into account for + equality checks and hashing anymore. + .. versionadded:: 21.1.0 *eq_key* and *order_key* + + For the full version history of the fields, see `attr.ib`. + """ + + __slots__ = ( + "name", + "default", + "validator", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "type", + "converter", + "kw_only", + "inherited", + "on_setattr", + ) + + def __init__( + self, + name, + default, + validator, + repr, + cmp, # XXX: unused, remove along with other cmp code. + hash, + init, + inherited, + metadata=None, + type=None, + converter=None, + kw_only=False, + eq=None, + eq_key=None, + order=None, + order_key=None, + on_setattr=None, + ): + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq_key or eq, order_key or order, True + ) + + # Cache this descriptor here to speed things up later. + bound_setattr = _obj_setattr.__get__(self, Attribute) + + # Despite the big red warning, people *do* instantiate `Attribute` + # themselves. + bound_setattr("name", name) + bound_setattr("default", default) + bound_setattr("validator", validator) + bound_setattr("repr", repr) + bound_setattr("eq", eq) + bound_setattr("eq_key", eq_key) + bound_setattr("order", order) + bound_setattr("order_key", order_key) + bound_setattr("hash", hash) + bound_setattr("init", init) + bound_setattr("converter", converter) + bound_setattr( + "metadata", + ( + metadata_proxy(metadata) + if metadata + else _empty_metadata_singleton + ), + ) + bound_setattr("type", type) + bound_setattr("kw_only", kw_only) + bound_setattr("inherited", inherited) + bound_setattr("on_setattr", on_setattr) + + def __setattr__(self, name, value): + raise FrozenInstanceError() + + @classmethod + def from_counting_attr(cls, name, ca, type=None): + # type holds the annotated value. deal with conflicts: + if type is None: + type = ca.type + elif ca.type is not None: + raise ValueError( + "Type annotation and type argument cannot both be present" + ) + inst_dict = { + k: getattr(ca, k) + for k in Attribute.__slots__ + if k + not in ( + "name", + "validator", + "default", + "type", + "inherited", + ) # exclude methods and deprecated alias + } + return cls( + name=name, + validator=ca._validator, + default=ca._default, + type=type, + cmp=None, + inherited=False, + **inst_dict + ) + + @property + def cmp(self): + """ + Simulate the presence of a cmp attribute and warn. + """ + warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=2) + + return self.eq and self.order + + # Don't use attr.evolve since fields(Attribute) doesn't work + def evolve(self, **changes): + """ + Copy *self* and apply *changes*. + + This works similarly to `attr.evolve` but that function does not work + with ``Attribute``. + + It is mainly meant to be used for `transform-fields`. + + .. versionadded:: 20.3.0 + """ + new = copy.copy(self) + + new._setattrs(changes.items()) + + return new + + # Don't use _add_pickle since fields(Attribute) doesn't work + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple( + getattr(self, name) if name != "metadata" else dict(self.metadata) + for name in self.__slots__ + ) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + self._setattrs(zip(self.__slots__, state)) + + def _setattrs(self, name_values_pairs): + bound_setattr = _obj_setattr.__get__(self, Attribute) + for name, value in name_values_pairs: + if name != "metadata": + bound_setattr(name, value) + else: + bound_setattr( + name, + metadata_proxy(value) + if value + else _empty_metadata_singleton, + ) + + +_a = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=(name != "metadata"), + init=True, + inherited=False, + ) + for name in Attribute.__slots__ +] + +Attribute = _add_hash( + _add_eq( + _add_repr(Attribute, attrs=_a), + attrs=[a for a in _a if a.name != "inherited"], + ), + attrs=[a for a in _a if a.hash and a.name != "inherited"], +) + + +class _CountingAttr(object): + """ + Intermediate representation of attributes that uses a counter to preserve + the order in which the attributes have been defined. + + *Internal* data structure of the attrs library. Running into is most + likely the result of a bug like a forgotten `@attr.s` decorator. + """ + + __slots__ = ( + "counter", + "_default", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "_validator", + "converter", + "type", + "kw_only", + "on_setattr", + ) + __attrs_attrs__ = tuple( + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=True, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ) + for name in ( + "counter", + "_default", + "repr", + "eq", + "order", + "hash", + "init", + "on_setattr", + ) + ) + ( + Attribute( + name="metadata", + default=None, + validator=None, + repr=True, + cmp=None, + hash=False, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ), + ) + cls_counter = 0 + + def __init__( + self, + default, + validator, + repr, + cmp, + hash, + init, + converter, + metadata, + type, + kw_only, + eq, + eq_key, + order, + order_key, + on_setattr, + ): + _CountingAttr.cls_counter += 1 + self.counter = _CountingAttr.cls_counter + self._default = default + self._validator = validator + self.converter = converter + self.repr = repr + self.eq = eq + self.eq_key = eq_key + self.order = order + self.order_key = order_key + self.hash = hash + self.init = init + self.metadata = metadata + self.type = type + self.kw_only = kw_only + self.on_setattr = on_setattr + + def validator(self, meth): + """ + Decorator that adds *meth* to the list of validators. + + Returns *meth* unchanged. + + .. versionadded:: 17.1.0 + """ + if self._validator is None: + self._validator = meth + else: + self._validator = and_(self._validator, meth) + return meth + + def default(self, meth): + """ + Decorator that allows to set the default for an attribute. + + Returns *meth* unchanged. + + :raises DefaultAlreadySetError: If default has been set before. + + .. versionadded:: 17.1.0 + """ + if self._default is not NOTHING: + raise DefaultAlreadySetError() + + self._default = Factory(meth, takes_self=True) + + return meth + + +_CountingAttr = _add_eq(_add_repr(_CountingAttr)) + + +class Factory(object): + """ + Stores a factory callable. + + If passed as the default value to `attrs.field`, the factory is used to + generate a new value. + + :param callable factory: A callable that takes either none or exactly one + mandatory positional argument depending on *takes_self*. + :param bool takes_self: Pass the partially initialized instance that is + being initialized as a positional argument. + + .. versionadded:: 17.1.0 *takes_self* + """ + + __slots__ = ("factory", "takes_self") + + def __init__(self, factory, takes_self=False): + """ + `Factory` is part of the default machinery so if we want a default + value here, we have to implement it ourselves. + """ + self.factory = factory + self.takes_self = takes_self + + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple(getattr(self, name) for name in self.__slots__) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + for name, value in zip(self.__slots__, state): + setattr(self, name, value) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in Factory.__slots__ +] + +Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) + + +def make_class(name, attrs, bases=(object,), **attributes_arguments): + """ + A quick way to create a new class called *name* with *attrs*. + + :param str name: The name for the new class. + + :param attrs: A list of names or a dictionary of mappings of names to + attributes. + + If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, + `collections.OrderedDict` otherwise), the order is deduced from + the order of the names or attributes inside *attrs*. Otherwise the + order of the definition of the attributes is used. + :type attrs: `list` or `dict` + + :param tuple bases: Classes that the new class will subclass. + + :param attributes_arguments: Passed unmodified to `attr.s`. + + :return: A new class with *attrs*. + :rtype: type + + .. versionadded:: 17.1.0 *bases* + .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + """ + if isinstance(attrs, dict): + cls_dict = attrs + elif isinstance(attrs, (list, tuple)): + cls_dict = dict((a, attrib()) for a in attrs) + else: + raise TypeError("attrs argument must be a dict or a list.") + + pre_init = cls_dict.pop("__attrs_pre_init__", None) + post_init = cls_dict.pop("__attrs_post_init__", None) + user_init = cls_dict.pop("__init__", None) + + body = {} + if pre_init is not None: + body["__attrs_pre_init__"] = pre_init + if post_init is not None: + body["__attrs_post_init__"] = post_init + if user_init is not None: + body["__init__"] = user_init + + type_ = new_class(name, bases, {}, lambda ns: ns.update(body)) + + # For pickling to work, the __module__ variable needs to be set to the + # frame where the class is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython). + try: + type_.__module__ = sys._getframe(1).f_globals.get( + "__name__", "__main__" + ) + except (AttributeError, ValueError): + pass + + # We do it here for proper warnings with meaningful stacklevel. + cmp = attributes_arguments.pop("cmp", None) + ( + attributes_arguments["eq"], + attributes_arguments["order"], + ) = _determine_attrs_eq_order( + cmp, + attributes_arguments.get("eq"), + attributes_arguments.get("order"), + True, + ) + + return _attrs(these=cls_dict, **attributes_arguments)(type_) + + +# These are required by within this module so we define them here and merely +# import into .validators / .converters. + + +@attrs(slots=True, hash=True) +class _AndValidator(object): + """ + Compose many validators to a single one. + """ + + _validators = attrib() + + def __call__(self, inst, attr, value): + for v in self._validators: + v(inst, attr, value) + + +def and_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators. + + :param callables validators: Arbitrary number of validators. + + .. versionadded:: 17.1.0 + """ + vals = [] + for validator in validators: + vals.extend( + validator._validators + if isinstance(validator, _AndValidator) + else [validator] + ) + + return _AndValidator(tuple(vals)) + + +def pipe(*converters): + """ + A converter that composes multiple converters into one. + + When called on a value, it runs all wrapped converters, returning the + *last* value. + + Type annotations will be inferred from the wrapped converters', if + they have any. + + :param callables converters: Arbitrary number of converters. + + .. versionadded:: 20.1.0 + """ + + def pipe_converter(val): + for converter in converters: + val = converter(val) + + return val + + if not PY2: + if not converters: + # If the converter list is empty, pipe_converter is the identity. + A = typing.TypeVar("A") + pipe_converter.__annotations__ = {"val": A, "return": A} + else: + # Get parameter type. + sig = None + try: + sig = inspect.signature(converters[0]) + except (ValueError, TypeError): # inspect failed + pass + if sig: + params = list(sig.parameters.values()) + if ( + params + and params[0].annotation is not inspect.Parameter.empty + ): + pipe_converter.__annotations__["val"] = params[ + 0 + ].annotation + # Get return type. + sig = None + try: + sig = inspect.signature(converters[-1]) + except (ValueError, TypeError): # inspect failed + pass + if sig and sig.return_annotation is not inspect.Signature().empty: + pipe_converter.__annotations__[ + "return" + ] = sig.return_annotation + + return pipe_converter diff --git a/openpype/vendor/python/python_2/attr/_next_gen.py b/openpype/vendor/python/python_2/attr/_next_gen.py new file mode 100644 index 0000000000..068253688c --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_next_gen.py @@ -0,0 +1,216 @@ +# SPDX-License-Identifier: MIT + +""" +These are Python 3.6+-only and keyword-only APIs that call `attr.s` and +`attr.ib` with different default values. +""" + + +from functools import partial + +from . import setters +from ._funcs import asdict as _asdict +from ._funcs import astuple as _astuple +from ._make import ( + NOTHING, + _frozen_setattrs, + _ng_default_on_setattr, + attrib, + attrs, +) +from .exceptions import UnannotatedAttributeError + + +def define( + maybe_cls=None, + *, + these=None, + repr=None, + hash=None, + init=None, + slots=True, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=None, + kw_only=False, + cache_hash=False, + auto_exc=True, + eq=None, + order=False, + auto_detect=True, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, +): + r""" + Define an ``attrs`` class. + + Differences to the classic `attr.s` that it uses underneath: + + - Automatically detect whether or not *auto_attribs* should be `True` + (c.f. *auto_attribs* parameter). + - If *frozen* is `False`, run converters and validators when setting an + attribute by default. + - *slots=True* (see :term:`slotted classes` for potentially surprising + behaviors) + - *auto_exc=True* + - *auto_detect=True* + - *order=False* + - *match_args=True* + - Some options that were only relevant on Python 2 or were kept around for + backwards-compatibility have been removed. + + Please note that these are all defaults and you can change them as you + wish. + + :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves + exactly like `attr.s`. If left `None`, `attr.s` will try to guess: + + 1. If any attributes are annotated and no unannotated `attrs.fields`\ s + are found, it assumes *auto_attribs=True*. + 2. Otherwise it assumes *auto_attribs=False* and tries to collect + `attrs.fields`\ s. + + For now, please refer to `attr.s` for the rest of the parameters. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + """ + + def do_it(cls, auto_attribs): + return attrs( + maybe_cls=cls, + these=these, + repr=repr, + hash=hash, + init=init, + slots=slots, + frozen=frozen, + weakref_slot=weakref_slot, + str=str, + auto_attribs=auto_attribs, + kw_only=kw_only, + cache_hash=cache_hash, + auto_exc=auto_exc, + eq=eq, + order=order, + auto_detect=auto_detect, + collect_by_mro=True, + getstate_setstate=getstate_setstate, + on_setattr=on_setattr, + field_transformer=field_transformer, + match_args=match_args, + ) + + def wrap(cls): + """ + Making this a wrapper ensures this code runs during class creation. + + We also ensure that frozen-ness of classes is inherited. + """ + nonlocal frozen, on_setattr + + had_on_setattr = on_setattr not in (None, setters.NO_OP) + + # By default, mutable classes convert & validate on setattr. + if frozen is False and on_setattr is None: + on_setattr = _ng_default_on_setattr + + # However, if we subclass a frozen class, we inherit the immutability + # and disable on_setattr. + for base_cls in cls.__bases__: + if base_cls.__setattr__ is _frozen_setattrs: + if had_on_setattr: + raise ValueError( + "Frozen classes can't use on_setattr " + "(frozen-ness was inherited)." + ) + + on_setattr = setters.NO_OP + break + + if auto_attribs is not None: + return do_it(cls, auto_attribs) + + try: + return do_it(cls, True) + except UnannotatedAttributeError: + return do_it(cls, False) + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but ``None`` if used as `@attrs()`. + if maybe_cls is None: + return wrap + else: + return wrap(maybe_cls) + + +mutable = define +frozen = partial(define, frozen=True, on_setattr=None) + + +def field( + *, + default=NOTHING, + validator=None, + repr=True, + hash=None, + init=True, + metadata=None, + converter=None, + factory=None, + kw_only=False, + eq=None, + order=None, + on_setattr=None, +): + """ + Identical to `attr.ib`, except keyword-only and with some arguments + removed. + + .. versionadded:: 20.1.0 + """ + return attrib( + default=default, + validator=validator, + repr=repr, + hash=hash, + init=init, + metadata=metadata, + converter=converter, + factory=factory, + kw_only=kw_only, + eq=eq, + order=order, + on_setattr=on_setattr, + ) + + +def asdict(inst, *, recurse=True, filter=None, value_serializer=None): + """ + Same as `attr.asdict`, except that collections types are always retained + and dict is always used as *dict_factory*. + + .. versionadded:: 21.3.0 + """ + return _asdict( + inst=inst, + recurse=recurse, + filter=filter, + value_serializer=value_serializer, + retain_collection_types=True, + ) + + +def astuple(inst, *, recurse=True, filter=None): + """ + Same as `attr.astuple`, except that collections types are always retained + and `tuple` is always used as the *tuple_factory*. + + .. versionadded:: 21.3.0 + """ + return _astuple( + inst=inst, recurse=recurse, filter=filter, retain_collection_types=True + ) diff --git a/openpype/vendor/python/python_2/attr/_version_info.py b/openpype/vendor/python/python_2/attr/_version_info.py new file mode 100644 index 0000000000..cdaeec37a1 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_version_info.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +from functools import total_ordering + +from ._funcs import astuple +from ._make import attrib, attrs + + +@total_ordering +@attrs(eq=False, order=False, slots=True, frozen=True) +class VersionInfo(object): + """ + A version object that can be compared to tuple of length 1--4: + + >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2) + True + >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1) + True + >>> vi = attr.VersionInfo(19, 2, 0, "final") + >>> vi < (19, 1, 1) + False + >>> vi < (19,) + False + >>> vi == (19, 2,) + True + >>> vi == (19, 2, 1) + False + + .. versionadded:: 19.2 + """ + + year = attrib(type=int) + minor = attrib(type=int) + micro = attrib(type=int) + releaselevel = attrib(type=str) + + @classmethod + def _from_version_string(cls, s): + """ + Parse *s* and return a _VersionInfo. + """ + v = s.split(".") + if len(v) == 3: + v.append("final") + + return cls( + year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3] + ) + + def _ensure_tuple(self, other): + """ + Ensure *other* is a tuple of a valid length. + + Returns a possibly transformed *other* and ourselves as a tuple of + the same length as *other*. + """ + + if self.__class__ is other.__class__: + other = astuple(other) + + if not isinstance(other, tuple): + raise NotImplementedError + + if not (1 <= len(other) <= 4): + raise NotImplementedError + + return astuple(self)[: len(other)], other + + def __eq__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + return us == them + + def __lt__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't + # have to do anything special with releaselevel for now. + return us < them diff --git a/openpype/vendor/python/python_2/attr/_version_info.pyi b/openpype/vendor/python/python_2/attr/_version_info.pyi new file mode 100644 index 0000000000..45ced08633 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_version_info.pyi @@ -0,0 +1,9 @@ +class VersionInfo: + @property + def year(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def micro(self) -> int: ... + @property + def releaselevel(self) -> str: ... diff --git a/openpype/vendor/python/python_2/attr/converters.py b/openpype/vendor/python/python_2/attr/converters.py new file mode 100644 index 0000000000..1fb6c05d7b --- /dev/null +++ b/openpype/vendor/python/python_2/attr/converters.py @@ -0,0 +1,155 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful converters. +""" + +from __future__ import absolute_import, division, print_function + +from ._compat import PY2 +from ._make import NOTHING, Factory, pipe + + +if not PY2: + import inspect + import typing + + +__all__ = [ + "default_if_none", + "optional", + "pipe", + "to_bool", +] + + +def optional(converter): + """ + A converter that allows an attribute to be optional. An optional attribute + is one which can be set to ``None``. + + Type annotations will be inferred from the wrapped converter's, if it + has any. + + :param callable converter: the converter that is used for non-``None`` + values. + + .. versionadded:: 17.1.0 + """ + + def optional_converter(val): + if val is None: + return None + return converter(val) + + if not PY2: + sig = None + try: + sig = inspect.signature(converter) + except (ValueError, TypeError): # inspect failed + pass + if sig: + params = list(sig.parameters.values()) + if params and params[0].annotation is not inspect.Parameter.empty: + optional_converter.__annotations__["val"] = typing.Optional[ + params[0].annotation + ] + if sig.return_annotation is not inspect.Signature.empty: + optional_converter.__annotations__["return"] = typing.Optional[ + sig.return_annotation + ] + + return optional_converter + + +def default_if_none(default=NOTHING, factory=None): + """ + A converter that allows to replace ``None`` values by *default* or the + result of *factory*. + + :param default: Value to be used if ``None`` is passed. Passing an instance + of `attrs.Factory` is supported, however the ``takes_self`` option + is *not*. + :param callable factory: A callable that takes no parameters whose result + is used if ``None`` is passed. + + :raises TypeError: If **neither** *default* or *factory* is passed. + :raises TypeError: If **both** *default* and *factory* are passed. + :raises ValueError: If an instance of `attrs.Factory` is passed with + ``takes_self=True``. + + .. versionadded:: 18.2.0 + """ + if default is NOTHING and factory is None: + raise TypeError("Must pass either `default` or `factory`.") + + if default is not NOTHING and factory is not None: + raise TypeError( + "Must pass either `default` or `factory` but not both." + ) + + if factory is not None: + default = Factory(factory) + + if isinstance(default, Factory): + if default.takes_self: + raise ValueError( + "`takes_self` is not supported by default_if_none." + ) + + def default_if_none_converter(val): + if val is not None: + return val + + return default.factory() + + else: + + def default_if_none_converter(val): + if val is not None: + return val + + return default + + return default_if_none_converter + + +def to_bool(val): + """ + Convert "boolean" strings (e.g., from env. vars.) to real booleans. + + Values mapping to :code:`True`: + + - :code:`True` + - :code:`"true"` / :code:`"t"` + - :code:`"yes"` / :code:`"y"` + - :code:`"on"` + - :code:`"1"` + - :code:`1` + + Values mapping to :code:`False`: + + - :code:`False` + - :code:`"false"` / :code:`"f"` + - :code:`"no"` / :code:`"n"` + - :code:`"off"` + - :code:`"0"` + - :code:`0` + + :raises ValueError: for any other value. + + .. versionadded:: 21.3.0 + """ + if isinstance(val, str): + val = val.lower() + truthy = {True, "true", "t", "yes", "y", "on", "1", 1} + falsy = {False, "false", "f", "no", "n", "off", "0", 0} + try: + if val in truthy: + return True + if val in falsy: + return False + except TypeError: + # Raised when "val" is not hashable (e.g., lists) + pass + raise ValueError("Cannot convert value to bool: {}".format(val)) diff --git a/openpype/vendor/python/python_2/attr/converters.pyi b/openpype/vendor/python/python_2/attr/converters.pyi new file mode 100644 index 0000000000..0f58088a37 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/converters.pyi @@ -0,0 +1,13 @@ +from typing import Callable, Optional, TypeVar, overload + +from . import _ConverterType + +_T = TypeVar("_T") + +def pipe(*validators: _ConverterType) -> _ConverterType: ... +def optional(converter: _ConverterType) -> _ConverterType: ... +@overload +def default_if_none(default: _T) -> _ConverterType: ... +@overload +def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ... +def to_bool(val: str) -> bool: ... diff --git a/openpype/vendor/python/python_2/attr/exceptions.py b/openpype/vendor/python/python_2/attr/exceptions.py new file mode 100644 index 0000000000..b2f1edc32a --- /dev/null +++ b/openpype/vendor/python/python_2/attr/exceptions.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + + +class FrozenError(AttributeError): + """ + A frozen/immutable instance or attribute have been attempted to be + modified. + + It mirrors the behavior of ``namedtuples`` by using the same error message + and subclassing `AttributeError`. + + .. versionadded:: 20.1.0 + """ + + msg = "can't set attribute" + args = [msg] + + +class FrozenInstanceError(FrozenError): + """ + A frozen instance has been attempted to be modified. + + .. versionadded:: 16.1.0 + """ + + +class FrozenAttributeError(FrozenError): + """ + A frozen attribute has been attempted to be modified. + + .. versionadded:: 20.1.0 + """ + + +class AttrsAttributeNotFoundError(ValueError): + """ + An ``attrs`` function couldn't find an attribute that the user asked for. + + .. versionadded:: 16.2.0 + """ + + +class NotAnAttrsClassError(ValueError): + """ + A non-``attrs`` class has been passed into an ``attrs`` function. + + .. versionadded:: 16.2.0 + """ + + +class DefaultAlreadySetError(RuntimeError): + """ + A default has been set using ``attr.ib()`` and is attempted to be reset + using the decorator. + + .. versionadded:: 17.1.0 + """ + + +class UnannotatedAttributeError(RuntimeError): + """ + A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type + annotation. + + .. versionadded:: 17.3.0 + """ + + +class PythonTooOldError(RuntimeError): + """ + It was attempted to use an ``attrs`` feature that requires a newer Python + version. + + .. versionadded:: 18.2.0 + """ + + +class NotCallableError(TypeError): + """ + A ``attr.ib()`` requiring a callable has been set with a value + that is not callable. + + .. versionadded:: 19.2.0 + """ + + def __init__(self, msg, value): + super(TypeError, self).__init__(msg, value) + self.msg = msg + self.value = value + + def __str__(self): + return str(self.msg) diff --git a/openpype/vendor/python/python_2/attr/exceptions.pyi b/openpype/vendor/python/python_2/attr/exceptions.pyi new file mode 100644 index 0000000000..f2680118b4 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/exceptions.pyi @@ -0,0 +1,17 @@ +from typing import Any + +class FrozenError(AttributeError): + msg: str = ... + +class FrozenInstanceError(FrozenError): ... +class FrozenAttributeError(FrozenError): ... +class AttrsAttributeNotFoundError(ValueError): ... +class NotAnAttrsClassError(ValueError): ... +class DefaultAlreadySetError(RuntimeError): ... +class UnannotatedAttributeError(RuntimeError): ... +class PythonTooOldError(RuntimeError): ... + +class NotCallableError(TypeError): + msg: str = ... + value: Any = ... + def __init__(self, msg: str, value: Any) -> None: ... diff --git a/openpype/vendor/python/python_2/attr/filters.py b/openpype/vendor/python/python_2/attr/filters.py new file mode 100644 index 0000000000..a1978a8775 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/filters.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful filters for `attr.asdict`. +""" + +from __future__ import absolute_import, division, print_function + +from ._compat import isclass +from ._make import Attribute + + +def _split_what(what): + """ + Returns a tuple of `frozenset`s of classes and attributes. + """ + return ( + frozenset(cls for cls in what if isclass(cls)), + frozenset(cls for cls in what if isinstance(cls, Attribute)), + ) + + +def include(*what): + """ + Include *what*. + + :param what: What to include. + :type what: `list` of `type` or `attrs.Attribute`\\ s + + :rtype: `callable` + """ + cls, attrs = _split_what(what) + + def include_(attribute, value): + return value.__class__ in cls or attribute in attrs + + return include_ + + +def exclude(*what): + """ + Exclude *what*. + + :param what: What to exclude. + :type what: `list` of classes or `attrs.Attribute`\\ s. + + :rtype: `callable` + """ + cls, attrs = _split_what(what) + + def exclude_(attribute, value): + return value.__class__ not in cls and attribute not in attrs + + return exclude_ diff --git a/openpype/vendor/python/python_2/attr/filters.pyi b/openpype/vendor/python/python_2/attr/filters.pyi new file mode 100644 index 0000000000..993866865e --- /dev/null +++ b/openpype/vendor/python/python_2/attr/filters.pyi @@ -0,0 +1,6 @@ +from typing import Any, Union + +from . import Attribute, _FilterType + +def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... +def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... diff --git a/openpype/vendor/python/python_2/attr/py.typed b/openpype/vendor/python/python_2/attr/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/vendor/python/python_2/attr/setters.py b/openpype/vendor/python/python_2/attr/setters.py new file mode 100644 index 0000000000..b1cbb5d83e --- /dev/null +++ b/openpype/vendor/python/python_2/attr/setters.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly used hooks for on_setattr. +""" + +from __future__ import absolute_import, division, print_function + +from . import _config +from .exceptions import FrozenAttributeError + + +def pipe(*setters): + """ + Run all *setters* and return the return value of the last one. + + .. versionadded:: 20.1.0 + """ + + def wrapped_pipe(instance, attrib, new_value): + rv = new_value + + for setter in setters: + rv = setter(instance, attrib, rv) + + return rv + + return wrapped_pipe + + +def frozen(_, __, ___): + """ + Prevent an attribute to be modified. + + .. versionadded:: 20.1.0 + """ + raise FrozenAttributeError() + + +def validate(instance, attrib, new_value): + """ + Run *attrib*'s validator on *new_value* if it has one. + + .. versionadded:: 20.1.0 + """ + if _config._run_validators is False: + return new_value + + v = attrib.validator + if not v: + return new_value + + v(instance, attrib, new_value) + + return new_value + + +def convert(instance, attrib, new_value): + """ + Run *attrib*'s converter -- if it has one -- on *new_value* and return the + result. + + .. versionadded:: 20.1.0 + """ + c = attrib.converter + if c: + return c(new_value) + + return new_value + + +NO_OP = object() +""" +Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. + +Does not work in `pipe` or within lists. + +.. versionadded:: 20.1.0 +""" diff --git a/openpype/vendor/python/python_2/attr/setters.pyi b/openpype/vendor/python/python_2/attr/setters.pyi new file mode 100644 index 0000000000..3f5603c2b0 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/setters.pyi @@ -0,0 +1,19 @@ +from typing import Any, NewType, NoReturn, TypeVar, cast + +from . import Attribute, _OnSetAttrType + +_T = TypeVar("_T") + +def frozen( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> NoReturn: ... +def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ... +def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ... + +# convert is allowed to return Any, because they can be chained using pipe. +def convert( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> Any: ... + +_NoOpType = NewType("_NoOpType", object) +NO_OP: _NoOpType diff --git a/openpype/vendor/python/python_2/attr/validators.py b/openpype/vendor/python/python_2/attr/validators.py new file mode 100644 index 0000000000..0b0c8342f2 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/validators.py @@ -0,0 +1,561 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful validators. +""" + +from __future__ import absolute_import, division, print_function + +import operator +import re + +from contextlib import contextmanager + +from ._config import get_run_validators, set_run_validators +from ._make import _AndValidator, and_, attrib, attrs +from .exceptions import NotCallableError + + +try: + Pattern = re.Pattern +except AttributeError: # Python <3.7 lacks a Pattern type. + Pattern = type(re.compile("")) + + +__all__ = [ + "and_", + "deep_iterable", + "deep_mapping", + "disabled", + "ge", + "get_disabled", + "gt", + "in_", + "instance_of", + "is_callable", + "le", + "lt", + "matches_re", + "max_len", + "optional", + "provides", + "set_disabled", +] + + +def set_disabled(disabled): + """ + Globally disable or enable running validators. + + By default, they are run. + + :param disabled: If ``True``, disable running all validators. + :type disabled: bool + + .. warning:: + + This function is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(not disabled) + + +def get_disabled(): + """ + Return a bool indicating whether validators are currently disabled or not. + + :return: ``True`` if validators are currently disabled. + :rtype: bool + + .. versionadded:: 21.3.0 + """ + return not get_run_validators() + + +@contextmanager +def disabled(): + """ + Context manager that disables running validators within its context. + + .. warning:: + + This context manager is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(False) + try: + yield + finally: + set_run_validators(True) + + +@attrs(repr=False, slots=True, hash=True) +class _InstanceOfValidator(object): + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not isinstance(value, self.type): + raise TypeError( + "'{name}' must be {type!r} (got {value!r} that is a " + "{actual!r}).".format( + name=attr.name, + type=self.type, + actual=value.__class__, + value=value, + ), + attr, + self.type, + value, + ) + + def __repr__(self): + return "".format( + type=self.type + ) + + +def instance_of(type): + """ + A validator that raises a `TypeError` if the initializer is called + with a wrong type for this particular attribute (checks are performed using + `isinstance` therefore it's also valid to pass a tuple of types). + + :param type: The type to check for. + :type type: type or tuple of types + + :raises TypeError: With a human readable error message, the attribute + (of type `attrs.Attribute`), the expected type, and the value it + got. + """ + return _InstanceOfValidator(type) + + +@attrs(repr=False, frozen=True, slots=True) +class _MatchesReValidator(object): + pattern = attrib() + match_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.match_func(value): + raise ValueError( + "'{name}' must match regex {pattern!r}" + " ({value!r} doesn't)".format( + name=attr.name, pattern=self.pattern.pattern, value=value + ), + attr, + self.pattern, + value, + ) + + def __repr__(self): + return "".format( + pattern=self.pattern + ) + + +def matches_re(regex, flags=0, func=None): + r""" + A validator that raises `ValueError` if the initializer is called + with a string that doesn't match *regex*. + + :param regex: a regex string or precompiled pattern to match against + :param int flags: flags that will be passed to the underlying re function + (default 0) + :param callable func: which underlying `re` function to call (options + are `re.fullmatch`, `re.search`, `re.match`, default + is ``None`` which means either `re.fullmatch` or an emulation of + it on Python 2). For performance reasons, they won't be used directly + but on a pre-`re.compile`\ ed pattern. + + .. versionadded:: 19.2.0 + .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. + """ + fullmatch = getattr(re, "fullmatch", None) + valid_funcs = (fullmatch, None, re.search, re.match) + if func not in valid_funcs: + raise ValueError( + "'func' must be one of {}.".format( + ", ".join( + sorted( + e and e.__name__ or "None" for e in set(valid_funcs) + ) + ) + ) + ) + + if isinstance(regex, Pattern): + if flags: + raise TypeError( + "'flags' can only be used with a string pattern; " + "pass flags to re.compile() instead" + ) + pattern = regex + else: + pattern = re.compile(regex, flags) + + if func is re.match: + match_func = pattern.match + elif func is re.search: + match_func = pattern.search + elif fullmatch: + match_func = pattern.fullmatch + else: # Python 2 fullmatch emulation (https://bugs.python.org/issue16203) + pattern = re.compile( + r"(?:{})\Z".format(pattern.pattern), pattern.flags + ) + match_func = pattern.match + + return _MatchesReValidator(pattern, match_func) + + +@attrs(repr=False, slots=True, hash=True) +class _ProvidesValidator(object): + interface = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.interface.providedBy(value): + raise TypeError( + "'{name}' must provide {interface!r} which {value!r} " + "doesn't.".format( + name=attr.name, interface=self.interface, value=value + ), + attr, + self.interface, + value, + ) + + def __repr__(self): + return "".format( + interface=self.interface + ) + + +def provides(interface): + """ + A validator that raises a `TypeError` if the initializer is called + with an object that does not provide the requested *interface* (checks are + performed using ``interface.providedBy(value)`` (see `zope.interface + `_). + + :param interface: The interface to check for. + :type interface: ``zope.interface.Interface`` + + :raises TypeError: With a human readable error message, the attribute + (of type `attrs.Attribute`), the expected interface, and the + value it got. + """ + return _ProvidesValidator(interface) + + +@attrs(repr=False, slots=True, hash=True) +class _OptionalValidator(object): + validator = attrib() + + def __call__(self, inst, attr, value): + if value is None: + return + + self.validator(inst, attr, value) + + def __repr__(self): + return "".format( + what=repr(self.validator) + ) + + +def optional(validator): + """ + A validator that makes an attribute optional. An optional attribute is one + which can be set to ``None`` in addition to satisfying the requirements of + the sub-validator. + + :param validator: A validator (or a list of validators) that is used for + non-``None`` values. + :type validator: callable or `list` of callables. + + .. versionadded:: 15.1.0 + .. versionchanged:: 17.1.0 *validator* can be a list of validators. + """ + if isinstance(validator, list): + return _OptionalValidator(_AndValidator(validator)) + return _OptionalValidator(validator) + + +@attrs(repr=False, slots=True, hash=True) +class _InValidator(object): + options = attrib() + + def __call__(self, inst, attr, value): + try: + in_options = value in self.options + except TypeError: # e.g. `1 in "abc"` + in_options = False + + if not in_options: + raise ValueError( + "'{name}' must be in {options!r} (got {value!r})".format( + name=attr.name, options=self.options, value=value + ) + ) + + def __repr__(self): + return "".format( + options=self.options + ) + + +def in_(options): + """ + A validator that raises a `ValueError` if the initializer is called + with a value that does not belong in the options provided. The check is + performed using ``value in options``. + + :param options: Allowed options. + :type options: list, tuple, `enum.Enum`, ... + + :raises ValueError: With a human readable error message, the attribute (of + type `attrs.Attribute`), the expected options, and the value it + got. + + .. versionadded:: 17.1.0 + """ + return _InValidator(options) + + +@attrs(repr=False, slots=False, hash=True) +class _IsCallableValidator(object): + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not callable(value): + message = ( + "'{name}' must be callable " + "(got {value!r} that is a {actual!r})." + ) + raise NotCallableError( + msg=message.format( + name=attr.name, value=value, actual=value.__class__ + ), + value=value, + ) + + def __repr__(self): + return "" + + +def is_callable(): + """ + A validator that raises a `attr.exceptions.NotCallableError` if the + initializer is called with a value for this particular attribute + that is not callable. + + .. versionadded:: 19.1.0 + + :raises `attr.exceptions.NotCallableError`: With a human readable error + message containing the attribute (`attrs.Attribute`) name, + and the value it got. + """ + return _IsCallableValidator() + + +@attrs(repr=False, slots=True, hash=True) +class _DeepIterable(object): + member_validator = attrib(validator=is_callable()) + iterable_validator = attrib( + default=None, validator=optional(is_callable()) + ) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.iterable_validator is not None: + self.iterable_validator(inst, attr, value) + + for member in value: + self.member_validator(inst, attr, member) + + def __repr__(self): + iterable_identifier = ( + "" + if self.iterable_validator is None + else " {iterable!r}".format(iterable=self.iterable_validator) + ) + return ( + "" + ).format( + iterable_identifier=iterable_identifier, + member=self.member_validator, + ) + + +def deep_iterable(member_validator, iterable_validator=None): + """ + A validator that performs deep validation of an iterable. + + :param member_validator: Validator to apply to iterable members + :param iterable_validator: Validator to apply to iterable itself + (optional) + + .. versionadded:: 19.1.0 + + :raises TypeError: if any sub-validators fail + """ + return _DeepIterable(member_validator, iterable_validator) + + +@attrs(repr=False, slots=True, hash=True) +class _DeepMapping(object): + key_validator = attrib(validator=is_callable()) + value_validator = attrib(validator=is_callable()) + mapping_validator = attrib(default=None, validator=optional(is_callable())) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.mapping_validator is not None: + self.mapping_validator(inst, attr, value) + + for key in value: + self.key_validator(inst, attr, key) + self.value_validator(inst, attr, value[key]) + + def __repr__(self): + return ( + "" + ).format(key=self.key_validator, value=self.value_validator) + + +def deep_mapping(key_validator, value_validator, mapping_validator=None): + """ + A validator that performs deep validation of a dictionary. + + :param key_validator: Validator to apply to dictionary keys + :param value_validator: Validator to apply to dictionary values + :param mapping_validator: Validator to apply to top-level mapping + attribute (optional) + + .. versionadded:: 19.1.0 + + :raises TypeError: if any sub-validators fail + """ + return _DeepMapping(key_validator, value_validator, mapping_validator) + + +@attrs(repr=False, frozen=True, slots=True) +class _NumberValidator(object): + bound = attrib() + compare_op = attrib() + compare_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.compare_func(value, self.bound): + raise ValueError( + "'{name}' must be {op} {bound}: {value}".format( + name=attr.name, + op=self.compare_op, + bound=self.bound, + value=value, + ) + ) + + def __repr__(self): + return "".format( + op=self.compare_op, bound=self.bound + ) + + +def lt(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number larger or equal to *val*. + + :param val: Exclusive upper bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<", operator.lt) + + +def le(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number greater than *val*. + + :param val: Inclusive upper bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<=", operator.le) + + +def ge(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number smaller than *val*. + + :param val: Inclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">=", operator.ge) + + +def gt(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number smaller or equal to *val*. + + :param val: Exclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">", operator.gt) + + +@attrs(repr=False, frozen=True, slots=True) +class _MaxLengthValidator(object): + max_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) > self.max_length: + raise ValueError( + "Length of '{name}' must be <= {max}: {len}".format( + name=attr.name, max=self.max_length, len=len(value) + ) + ) + + def __repr__(self): + return "".format(max=self.max_length) + + +def max_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is longer than *length*. + + :param int length: Maximum length of the string or iterable + + .. versionadded:: 21.3.0 + """ + return _MaxLengthValidator(length) diff --git a/openpype/vendor/python/python_2/attr/validators.pyi b/openpype/vendor/python/python_2/attr/validators.pyi new file mode 100644 index 0000000000..5e00b85433 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/validators.pyi @@ -0,0 +1,78 @@ +from typing import ( + Any, + AnyStr, + Callable, + Container, + ContextManager, + Iterable, + List, + Mapping, + Match, + Optional, + Pattern, + Tuple, + Type, + TypeVar, + Union, + overload, +) + +from . import _ValidatorType + +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") +_I = TypeVar("_I", bound=Iterable) +_K = TypeVar("_K") +_V = TypeVar("_V") +_M = TypeVar("_M", bound=Mapping) + +def set_disabled(run: bool) -> None: ... +def get_disabled() -> bool: ... +def disabled() -> ContextManager[None]: ... + +# To be more precise on instance_of use some overloads. +# If there are more than 3 items in the tuple then we fall back to Any +@overload +def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ... +@overload +def instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ... +@overload +def instance_of( + type: Tuple[Type[_T1], Type[_T2]] +) -> _ValidatorType[Union[_T1, _T2]]: ... +@overload +def instance_of( + type: Tuple[Type[_T1], Type[_T2], Type[_T3]] +) -> _ValidatorType[Union[_T1, _T2, _T3]]: ... +@overload +def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... +def provides(interface: Any) -> _ValidatorType[Any]: ... +def optional( + validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] +) -> _ValidatorType[Optional[_T]]: ... +def in_(options: Container[_T]) -> _ValidatorType[_T]: ... +def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... +def matches_re( + regex: Union[Pattern[AnyStr], AnyStr], + flags: int = ..., + func: Optional[ + Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]] + ] = ..., +) -> _ValidatorType[AnyStr]: ... +def deep_iterable( + member_validator: _ValidatorType[_T], + iterable_validator: Optional[_ValidatorType[_I]] = ..., +) -> _ValidatorType[_I]: ... +def deep_mapping( + key_validator: _ValidatorType[_K], + value_validator: _ValidatorType[_V], + mapping_validator: Optional[_ValidatorType[_M]] = ..., +) -> _ValidatorType[_M]: ... +def is_callable() -> _ValidatorType[_T]: ... +def lt(val: _T) -> _ValidatorType[_T]: ... +def le(val: _T) -> _ValidatorType[_T]: ... +def ge(val: _T) -> _ValidatorType[_T]: ... +def gt(val: _T) -> _ValidatorType[_T]: ... +def max_len(length: int) -> _ValidatorType[_T]: ... diff --git a/openpype/vendor/python/python_2/attrs/__init__.py b/openpype/vendor/python/python_2/attrs/__init__.py new file mode 100644 index 0000000000..a704b8b56b --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/__init__.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: MIT + +from attr import ( + NOTHING, + Attribute, + Factory, + __author__, + __copyright__, + __description__, + __doc__, + __email__, + __license__, + __title__, + __url__, + __version__, + __version_info__, + assoc, + cmp_using, + define, + evolve, + field, + fields, + fields_dict, + frozen, + has, + make_class, + mutable, + resolve_types, + validate, +) +from attr._next_gen import asdict, astuple + +from . import converters, exceptions, filters, setters, validators + + +__all__ = [ + "__author__", + "__copyright__", + "__description__", + "__doc__", + "__email__", + "__license__", + "__title__", + "__url__", + "__version__", + "__version_info__", + "asdict", + "assoc", + "astuple", + "Attribute", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "Factory", + "field", + "fields_dict", + "fields", + "filters", + "frozen", + "has", + "make_class", + "mutable", + "NOTHING", + "resolve_types", + "setters", + "validate", + "validators", +] diff --git a/openpype/vendor/python/python_2/attrs/__init__.pyi b/openpype/vendor/python/python_2/attrs/__init__.pyi new file mode 100644 index 0000000000..7426fa5ddb --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/__init__.pyi @@ -0,0 +1,63 @@ +from typing import ( + Any, + Callable, + Dict, + Mapping, + Optional, + Sequence, + Tuple, + Type, +) + +# Because we need to type our own stuff, we have to make everything from +# attr explicitly public too. +from attr import __author__ as __author__ +from attr import __copyright__ as __copyright__ +from attr import __description__ as __description__ +from attr import __email__ as __email__ +from attr import __license__ as __license__ +from attr import __title__ as __title__ +from attr import __url__ as __url__ +from attr import __version__ as __version__ +from attr import __version_info__ as __version_info__ +from attr import _FilterType +from attr import assoc as assoc +from attr import Attribute as Attribute +from attr import define as define +from attr import evolve as evolve +from attr import Factory as Factory +from attr import exceptions as exceptions +from attr import field as field +from attr import fields as fields +from attr import fields_dict as fields_dict +from attr import frozen as frozen +from attr import has as has +from attr import make_class as make_class +from attr import mutable as mutable +from attr import NOTHING as NOTHING +from attr import resolve_types as resolve_types +from attr import setters as setters +from attr import validate as validate +from attr import validators as validators + +# TODO: see definition of attr.asdict/astuple +def asdict( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + dict_factory: Type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Optional[ + Callable[[type, Attribute[Any], Any], Any] + ] = ..., + tuple_keys: bool = ..., +) -> Dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + tuple_factory: Type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> Tuple[Any, ...]: ... diff --git a/openpype/vendor/python/python_2/attrs/converters.py b/openpype/vendor/python/python_2/attrs/converters.py new file mode 100644 index 0000000000..edfa8d3c16 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/converters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.converters import * # noqa diff --git a/openpype/vendor/python/python_2/attrs/exceptions.py b/openpype/vendor/python/python_2/attrs/exceptions.py new file mode 100644 index 0000000000..bd9efed202 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/exceptions.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.exceptions import * # noqa diff --git a/openpype/vendor/python/python_2/attrs/filters.py b/openpype/vendor/python/python_2/attrs/filters.py new file mode 100644 index 0000000000..52959005b0 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/filters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.filters import * # noqa diff --git a/openpype/vendor/python/python_2/attrs/py.typed b/openpype/vendor/python/python_2/attrs/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/vendor/python/python_2/attrs/setters.py b/openpype/vendor/python/python_2/attrs/setters.py new file mode 100644 index 0000000000..9b50770804 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/setters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.setters import * # noqa diff --git a/openpype/vendor/python/python_2/attrs/validators.py b/openpype/vendor/python/python_2/attrs/validators.py new file mode 100644 index 0000000000..ab2c9b3024 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/validators.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.validators import * # noqa From c6383837e0c094a4172c6895db768a3d3ccebc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:00:46 +0200 Subject: [PATCH 118/155] :recycle: remove unnecessary type hint Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- igniter/bootstrap_repos.py | 1 - 1 file changed, 1 deletion(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index dfcca2cf33..c5003b062e 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -614,7 +614,6 @@ class OpenPypeVersion(semver.VersionInfo): return None all_versions.sort() - latest_version: OpenPypeVersion return all_versions[-1] @classmethod From e5b1cc59bdccc2175364ae24cdddb7eb40a7c2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:37:40 +0200 Subject: [PATCH 119/155] :bug: missing comma Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index cece8ee22b..67b5f2496b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -45,7 +45,7 @@ def get_ocio_config_path(profile_folder): os.environ["OPENPYPE_ROOT"], "vendor", "bin", - "ocioconfig" + "ocioconfig", "OpenColorIOConfigs", profile_folder, "config.ocio" From 4193b54700c42405ffa22e5353985202ce858ee2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 18:36:33 +0200 Subject: [PATCH 120/155] added more information when auto sync is turned on/off --- .../event_sync_to_avalon.py | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py index a4e791aaf0..738181dc9a 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -697,13 +697,22 @@ class SyncToAvalonEvent(BaseEvent): continue auto_sync = changes[CUST_ATTR_AUTO_SYNC]["new"] - if auto_sync == "1": + turned_on = auto_sync == "1" + ft_project = self.cur_project + username = self._get_username(session, event) + message = ( + "Auto sync was turned {} for project \"{}\" by \"{}\"." + ).format( + "on" if turned_on else "off", + ft_project["full_name"], + username + ) + if turned_on: + message += " Triggering syncToAvalon action." + self.log.debug(message) + + if turned_on: # Trigger sync to avalon action if auto sync was turned on - ft_project = self.cur_project - self.log.debug(( - "Auto sync was turned on for project <{}>." - " Triggering syncToAvalon action." - ).format(ft_project["full_name"])) selection = [{ "entityId": ft_project["id"], "entityType": "show" @@ -851,6 +860,26 @@ class SyncToAvalonEvent(BaseEvent): self.report() return True + def _get_username(self, session, event): + username = "Unknown" + event_source = event.get("source") + if not event_source: + return username + user_info = event_source.get("user") + if not user_info: + return username + user_id = user_info.get("id") + if not user_id: + return username + + user_entity = session.query( + "User where id is {}".format(user_id) + ).first() + if user_entity: + username = user_entity["username"] or username + return username + + def process_removed(self): """ Handles removed entities (not removed tasks - handle separately). From 61e8d7e9f1fbffd91e268ef3ff721cc136395f27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:14:01 +0200 Subject: [PATCH 121/155] use 'get_projects' instead of 'projects' method on AvalonMongoDB --- .../modules/kitsu/utils/update_zou_with_op.py | 8 +++- .../modules/sync_server/sync_server_module.py | 9 ++-- openpype/tools/launcher/models.py | 3 +- openpype/tools/libraryloader/app.py | 4 +- .../project_manager/project_manager/model.py | 7 +--- openpype/tools/sceneinventory/window.py | 6 +-- openpype/tools/utils/models.py | 41 ++++++++++--------- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index 57d7094e95..da924aa5ee 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -6,7 +6,11 @@ from typing import List import gazu from pymongo import UpdateOne -from openpype.client import get_project, get_assets +from openpype.client import ( + get_projects, + get_project, + get_assets, +) from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings from openpype.modules.kitsu.utils.credentials import validate_credentials @@ -37,7 +41,7 @@ def sync_zou(login: str, password: str): dbcon = AvalonMongoDB() dbcon.install() - op_projects = [p for p in dbcon.projects()] + op_projects = list(get_projects()) for project_doc in op_projects: sync_zou_from_op_project(project_doc["name"], dbcon, project_doc) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 8fdfab9c2e..c7f9484e55 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -6,7 +6,7 @@ import platform import copy from collections import deque, defaultdict - +from openpype.client import get_projects from openpype.modules import OpenPypeModule from openpype_interfaces import ITrayModule from openpype.settings import ( @@ -913,7 +913,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): enabled_projects = [] if self.enabled: - for project in self.connection.projects(projection={"name": 1}): + for project in get_projects(fields=["name"]): project_name = project["name"] if self.is_project_enabled(project_name): enabled_projects.append(project_name) @@ -1242,10 +1242,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def _prepare_sync_project_settings(self, exclude_locals): sync_project_settings = {} system_sites = self.get_all_site_configs() - project_docs = self.connection.projects( - projection={"name": 1}, - only_active=True - ) + project_docs = get_projects(fields=["name"]) for project_doc in project_docs: project_name = project_doc["name"] sites = copy.deepcopy(system_sites) # get all configured sites diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 3f899cc05e..6d40d21f96 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -10,6 +10,7 @@ from Qt import QtCore, QtGui import qtawesome from openpype.client import ( + get_projects, get_project, get_assets, ) @@ -527,7 +528,7 @@ class LauncherModel(QtCore.QObject): current_project = self.project_name project_names = set() project_docs_by_name = {} - for project_doc in self._dbcon.projects(only_active=True): + for project_doc in get_projects(): project_name = project_doc["name"] project_names.add(project_name) project_docs_by_name[project_name] = project_doc diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 5f4d10d796..d2af1b7151 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -3,7 +3,7 @@ import sys from Qt import QtWidgets, QtCore, QtGui from openpype import style -from openpype.client import get_project +from openpype.client import get_projects, get_project from openpype.pipeline import AvalonMongoDB from openpype.tools.utils import lib as tools_lib from openpype.tools.loader.widgets import ( @@ -239,7 +239,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): def get_filtered_projects(self): projects = list() - for project in self.dbcon.projects(): + for project in get_projects(fields=["name", "data.library_project"]): is_library = project.get("data", {}).get("library_project", False) if ( (is_library and self.show_libraries) or diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index c5bde5aaec..3aaee75698 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -8,6 +8,7 @@ from pymongo import UpdateOne, DeleteOne from Qt import QtCore, QtGui from openpype.client import ( + get_projects, get_project, get_assets, get_asset_ids_with_subsets, @@ -54,12 +55,8 @@ class ProjectModel(QtGui.QStandardItemModel): self._items_by_name[None] = none_project new_project_items.append(none_project) - project_docs = self.dbcon.projects( - projection={"name": 1}, - only_active=True - ) project_names = set() - for project_doc in project_docs: + for project_doc in get_projects(fields=["name"]): project_name = project_doc.get("name") if not project_name: continue diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 054c2a2daa..463280b71c 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -4,8 +4,9 @@ import sys from Qt import QtWidgets, QtCore import qtawesome -from openpype.pipeline import legacy_io from openpype import style +from openpype.client import get_projects +from openpype.pipeline import legacy_io from openpype.tools.utils.delegates import VersionDelegate from openpype.tools.utils.lib import ( qt_app_context, @@ -195,8 +196,7 @@ def show(root=None, debug=False, parent=None, items=None): if not os.environ.get("AVALON_PROJECT"): any_project = next( - project for project in legacy_io.projects() - if project.get("active", True) is not False + project for project in get_projects() ) project_name = any_project["name"] diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index 8991614fe1..1faccef4dd 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -3,6 +3,7 @@ import logging import Qt from Qt import QtCore, QtGui +from openpype.client import get_projects from .constants import ( PROJECT_IS_ACTIVE_ROLE, PROJECT_NAME_ROLE, @@ -296,29 +297,29 @@ class ProjectModel(QtGui.QStandardItemModel): self._default_item = item project_names = set() - if self.dbcon is not None: - for project_doc in self.dbcon.projects( - projection={"name": 1, "data.active": 1}, - only_active=self._only_active - ): - project_name = project_doc["name"] - project_names.add(project_name) - if project_name in self._items_by_name: - item = self._items_by_name[project_name] - else: - item = QtGui.QStandardItem(project_name) + project_docs = get_projects( + inactive=not self._only_active, + fields=["name", "data.active"] + ) + for project_doc in project_docs: + project_name = project_doc["name"] + project_names.add(project_name) + if project_name in self._items_by_name: + item = self._items_by_name[project_name] + else: + item = QtGui.QStandardItem(project_name) - self._items_by_name[project_name] = item - new_items.append(item) + self._items_by_name[project_name] = item + new_items.append(item) - is_active = project_doc.get("data", {}).get("active", True) - item.setData(project_name, PROJECT_NAME_ROLE) - item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + is_active = project_doc.get("data", {}).get("active", True) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) - if not is_active: - font = item.font() - font.setItalic(True) - item.setFont(font) + if not is_active: + font = item.font() + font.setItalic(True) + item.setFont(font) root_item = self.invisibleRootItem() for project_name in tuple(self._items_by_name.keys()): From 260ef9999516d437ad399a0b746ff73632106314 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:15:42 +0200 Subject: [PATCH 122/155] removed unused code --- openpype/tools/utils/lib.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 2169cf8ef1..99d8c75ab4 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -443,10 +443,6 @@ class FamilyConfigCache: if profiles: # Make sure connection is installed # - accessing attribute which does not have auto-install - self.dbcon.install() - database = getattr(self.dbcon, "database", None) - if database is None: - database = self.dbcon._database asset_doc = get_asset_by_name( project_name, asset_name, fields=["data.tasks"] ) or {} From e86ea84da897f0ecd6608bdfff460c742a10042e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:18:37 +0200 Subject: [PATCH 123/155] use 'get_projects' in standalone publisher --- openpype/tools/standalonepublish/widgets/widget_asset.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 73114f7960..77d756a606 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtCore import qtawesome from openpype.client import ( + get_projects, get_project, get_asset_by_id, ) @@ -291,9 +292,7 @@ class AssetWidget(QtWidgets.QWidget): def _set_projects(self): project_names = list() - for doc in self.dbcon.projects(projection={"name": 1}, - only_active=True): - + for doc in get_projects(fields=["name"]): project_name = doc.get("name") if project_name: project_names.append(project_name) @@ -320,8 +319,7 @@ class AssetWidget(QtWidgets.QWidget): def on_project_change(self): projects = list() - for project in self.dbcon.projects(projection={"name": 1}, - only_active=True): + for project in get_projects(fields=["name"]): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: From 68ca5898920d79c93bc51e699f319e5987019063 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:18:50 +0200 Subject: [PATCH 124/155] use 'get_projects' in settings --- openpype/tools/settings/settings/widgets.py | 60 ++++++++------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 88d923c16a..1a4a6877b0 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -3,6 +3,7 @@ import uuid from Qt import QtWidgets, QtCore, QtGui import qtawesome +from openpype.client import get_projects from openpype.pipeline import AvalonMongoDB from openpype.style import get_objected_colors from openpype.tools.utils.widgets import ImageButton @@ -783,8 +784,6 @@ class ProjectModel(QtGui.QStandardItemModel): self.setColumnCount(2) - self.dbcon = None - self._only_active = only_active self._default_item = None self._items_by_name = {} @@ -828,9 +827,6 @@ class ProjectModel(QtGui.QStandardItemModel): index = self.index(index.row(), 0, index.parent()) return super(ProjectModel, self).flags(index) - def set_dbcon(self, dbcon): - self.dbcon = dbcon - def refresh(self): # Change id of versions refresh self._version_refresh_id = uuid.uuid4() @@ -846,31 +842,30 @@ class ProjectModel(QtGui.QStandardItemModel): self._default_item.setData("", PROJECT_VERSION_ROLE) project_names = set() - if self.dbcon is not None: - for project_doc in self.dbcon.projects( - projection={"name": 1, "data.active": 1}, - only_active=self._only_active - ): - project_name = project_doc["name"] - project_names.add(project_name) - if project_name in self._items_by_name: - item = self._items_by_name[project_name] - else: - item = QtGui.QStandardItem(project_name) + for project_doc in get_projects( + inactive=not self._only_active, + fields=["name", "data.active"] + ): + project_name = project_doc["name"] + project_names.add(project_name) + if project_name in self._items_by_name: + item = self._items_by_name[project_name] + else: + item = QtGui.QStandardItem(project_name) - self._items_by_name[project_name] = item - new_items.append(item) + self._items_by_name[project_name] = item + new_items.append(item) - is_active = project_doc.get("data", {}).get("active", True) - item.setData(project_name, PROJECT_NAME_ROLE) - item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) - item.setData("", PROJECT_VERSION_ROLE) - item.setData(False, PROJECT_IS_SELECTED_ROLE) + is_active = project_doc.get("data", {}).get("active", True) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + item.setData("", PROJECT_VERSION_ROLE) + item.setData(False, PROJECT_IS_SELECTED_ROLE) - if not is_active: - font = item.font() - font.setItalic(True) - item.setFont(font) + if not is_active: + font = item.font() + font.setItalic(True) + item.setFont(font) root_item = self.invisibleRootItem() for project_name in tuple(self._items_by_name.keys()): @@ -1067,8 +1062,6 @@ class ProjectListWidget(QtWidgets.QWidget): self.project_model = project_model self.inactive_chk = inactive_chk - self.dbcon = None - def set_entity(self, entity): self._entity = entity @@ -1211,15 +1204,6 @@ class ProjectListWidget(QtWidgets.QWidget): selected_project = index.data(PROJECT_NAME_ROLE) break - if not self.dbcon: - try: - self.dbcon = AvalonMongoDB() - self.dbcon.install() - except Exception: - self.dbcon = None - self.current_project = None - - self.project_model.set_dbcon(self.dbcon) self.project_model.refresh() self.project_proxy.sort(0) From cfb14d32b50920d06fbfc6d1f74da2798910b3da Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 15:42:25 +0200 Subject: [PATCH 125/155] Show dialog if installed version is not compatible in UI mode --- start.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/start.py b/start.py index 084eb7451a..d1198a85e4 100644 --- a/start.py +++ b/start.py @@ -748,12 +748,21 @@ def _find_frozen_openpype(use_version: str = None, _initialize_environment(openpype_version) return version_path + in_headless_mode = os.getenv("OPENPYPE_HEADLESS_MODE") == "1" if not installed_version.is_compatible(openpype_version): - raise OpenPypeVersionIncompatible( - ( - f"Latest version found {openpype_version} is not " - f"compatible with currently running {installed_version}" + message = "Version {} is not compatible with installed version {}." + # Show UI to user + if not in_headless_mode: + igniter.show_message_dialog( + "Incompatible OpenPype installation", + message.format( + "{}".format(openpype_version), + "{}".format(installed_version) + ) ) + # Raise incompatible error + raise OpenPypeVersionIncompatible( + message.format(openpype_version, installed_version) ) # test if latest detected is installed (in user data dir) @@ -768,7 +777,7 @@ def _find_frozen_openpype(use_version: str = None, if not is_inside: # install latest version to user data dir - if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": + if in_headless_mode: version_path = bootstrap.install_version( openpype_version, force=True ) From e0c7ba861733e0cf4ec9087fdadcfe6b0d729aea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 17:53:16 +0200 Subject: [PATCH 126/155] added new plugin which change task status for instances if they are rendered on farm --- .../publish/integrate_ftrack_farm_status.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py new file mode 100644 index 0000000000..ecf258a870 --- /dev/null +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -0,0 +1,129 @@ +import pyblish.api +from openpype.lib import profiles_filtering + + +class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): + """Change task status when should be published on farm. + + Instance which has set "farm" key in data to 'True' is considered as will + be rendered on farm thus it's status should be changed. + """ + + order = pyblish.api.IntegratorOrder + 0.48 + label = "Integrate Ftrack Component" + families = ["ftrack"] + + farm_status_profiles = [] + + def process(self, context): + # Quick end + if not self.farm_status_profiles: + project_name = context.data["projectName"] + self.log.info(( + "Status profiles are not filled for project \"{}\". Skipping" + ).format(project_name)) + return + + filtered_instances = self.filter_instances(context) + instances_with_status_names = self.get_instances_with_statuse_names( + context, filtered_instances + ) + if instances_with_status_names: + self.fill_statuses(context, instances_with_status_names) + + def filter_instances(self, context): + filtered_instances = [] + for instance in context: + subset_name = instance.data["subset"] + msg_start = "SKipping instance {}.".format(subset_name) + if not instance.data.get("farm"): + self.log.debug( + "{} Won't be rendered on farm.".format(msg_start) + ) + continue + + task_entity = instance.data.get("ftrackTask") + if not task_entity: + self.log.debug( + "{} Does not have filled task".format(msg_start) + ) + continue + + filtered_instances.append(instance) + return filtered_instances + + def get_instances_with_statuse_names(self, context, instances): + instances_with_status_names = [] + for instance in instances: + family = instance.data["family"] + subset_name = instance.data["subset"] + task_entity = instance.data["ftrackTask"] + host_name = context.data["hostName"] + task_name = task_entity["name"] + task_type = task_entity["type"]["name"] + status_profile = profiles_filtering( + self.farm_status_profiles, + { + "hosts": host_name, + "task_types": task_type, + "task_names": task_name, + "families": family, + "subsets": subset_name, + }, + logger=self.log + ) + if not status_profile: + # There already is log in 'profiles_filtering' + continue + + status_name = status_profile["status_name"] + if status_name: + instances_with_status_names.append((instance, status_name)) + return instances_with_status_names + + def fill_statuses(self, context, instances_with_status_names): + # Prepare available task statuses on the project + project_name = context.data["projectName"] + session = context.data["ftrackSession"] + project_entity = session.query(( + "select project_schema from Project where full_name is \"{}\"" + ).format(project_name)).one() + project_schema = project_entity["project_schema"] + task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + + # Keep track if anything has changed + status_changed = False + found_status_id_by_status_name = {} + for item in instances_with_status_names: + instance, status_name = item + + status_name_low = status_name.lower() + status_id = found_status_id_by_status_name.get(status_name_low) + + if status_id is None: + # Skip if status name was already tried to be found + if status_name_low in found_status_id_by_status_name: + continue + + for status in task_workflow_statuses: + if status["name"].lower() == status_name_low: + status_id = status["id"] + break + + # Store the result to be reused in following instances + found_status_id_by_status_name[status_name_low] = status_id + + if status_id is None: + self.log.warning(( + "Status \"{}\" is not available on project \"{}\"" + ).format(status_name, project_name)) + continue + + # Change task status id + task_entity = instance.data["ftrackTask"] + if status_id != task_entity["status_id"]: + task_entity["status_id"] = status_id + status_changed = True + + if status_changed: + session.commit() From 41dd9e84f574663aef840596fa4e4c8a37a6a49b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 17:53:27 +0200 Subject: [PATCH 127/155] added settings schema for new plugin --- .../defaults/project_settings/ftrack.json | 3 + .../schema_project_ftrack.json | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 3e86581a03..610c85d232 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -489,6 +489,9 @@ }, "keep_first_subset_name_for_review": true, "asset_versions_status_profiles": [] + }, + "IntegrateFtrackFarmStatus": { + "farm_status_profiles": [] } } } \ No newline at end of file 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 c06bec0f58..a821b1de76 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1003,6 +1003,66 @@ } } ] + }, + { + "type": "dict", + "key": "IntegrateFtrackFarmStatus", + "label": "Integrate Ftrack Farm Status", + "children": [ + { + "type": "label", + "label": "Change status of task when it's subset is rendered on farm" + }, + { + "type": "list", + "collapsible": true, + "key": "farm_status_profiles", + "label": "Farm status profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "status_name", + "label": "Status name", + "type": "text" + } + ] + } + } + ] } ] } From ccaef43535dd0d80c3184a325f51bfaea8409d75 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 10:32:11 +0200 Subject: [PATCH 128/155] changed description label --- .../entities/schemas/projects_schema/schema_project_ftrack.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a821b1de76..1967a1150f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1011,7 +1011,7 @@ "children": [ { "type": "label", - "label": "Change status of task when it's subset is rendered on farm" + "label": "Change status of task when it's subset is submitted to farm" }, { "type": "list", From 8d65c65fc9ebcccc8d58fe3b55e7cb81b4706106 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Wed, 17 Aug 2022 11:34:00 +0300 Subject: [PATCH 129/155] Remove unused attribute. Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 2f09aaee87..668cb57292 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -71,7 +71,6 @@ class CreateRender(plugin.Creator): label = "Render" family = "rendering" icon = "eye" - enable_all_lights = True _token = None _user = None _password = None From 41ac0d65c4c7fe145cf4347e1faf5af6b5b7dfa6 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Wed, 17 Aug 2022 11:35:10 +0300 Subject: [PATCH 130/155] Fix bug in default. Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 668cb57292..5418ec1f2f 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -223,7 +223,7 @@ class CreateRender(plugin.Creator): self._project_settings.get( "maya", {}).get( "RenderSettings", {}).get( - "enable_all_lights", {}) + "enable_all_lights", False) ) # Disable for now as this feature is not working yet # self.data["assScene"] = False From 7deb3079247f56ba606b008c462099f18a73ae74 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Wed, 17 Aug 2022 11:35:26 +0300 Subject: [PATCH 131/155] Fix bug in default. Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/validate_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 93ef7d7af7..f19c0bff36 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -245,7 +245,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): settings_lights_flag = instance.context.data["project_settings"].get( "maya", {}).get( "RenderSettings", {}).get( - "enable_all_lights", {}) + "enable_all_lights", False) instance_lights_flag = instance.data.get("renderSetupIncludeLights") if settings_lights_flag != instance_lights_flag: From 2f3e6a73e3f8d130fc639cd1c5c1429e4957ea2a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 10:48:50 +0200 Subject: [PATCH 132/155] Change label of plugin --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index ecf258a870..f725de3144 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -10,7 +10,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): """ order = pyblish.api.IntegratorOrder + 0.48 - label = "Integrate Ftrack Component" + label = "Integrate Ftrack Farm Status" families = ["ftrack"] farm_status_profiles = [] @@ -35,7 +35,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): filtered_instances = [] for instance in context: subset_name = instance.data["subset"] - msg_start = "SKipping instance {}.".format(subset_name) + msg_start = "Skipping instance {}.".format(subset_name) if not instance.data.get("farm"): self.log.debug( "{} Won't be rendered on farm.".format(msg_start) From 7095bff502f13498bb1dd7a7a173693bf43e72dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:04:58 +0200 Subject: [PATCH 133/155] set "farm" to true in maya render colletor --- openpype/hosts/maya/plugins/publish/collect_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index c3e6c98020..0d45ad4f9e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -354,6 +354,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): instance = context.create_instance(expected_layer_name) instance.data["label"] = label + instance.data["farm"] = True instance.data.update(data) self.log.debug("data: {}".format(json.dumps(data, indent=4))) From 58f19f15f4a2c3d4ad6d0dd71089c0357904dcd9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:05:04 +0200 Subject: [PATCH 134/155] skip disabled instances --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index f725de3144..fcbe71e0ac 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -34,6 +34,9 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): def filter_instances(self, context): filtered_instances = [] for instance in context: + # Skip disabled instances + if instance.data.get("publish") is False: + continue subset_name = instance.data["subset"] msg_start = "Skipping instance {}.".format(subset_name) if not instance.data.get("farm"): From 346e3b8300e01ac8b3ab4e2c52a7d0c25a169d33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:11:39 +0200 Subject: [PATCH 135/155] removed families filter --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index fcbe71e0ac..24f784f83d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -11,7 +11,6 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder + 0.48 label = "Integrate Ftrack Farm Status" - families = ["ftrack"] farm_status_profiles = [] From 95c19cc412ecbfd0caf42e06ada91640e8da5885 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:22:56 +0200 Subject: [PATCH 136/155] fill context entities in all instances --- .../plugins/publish/collect_ftrack_api.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index 14da188150..99a555014e 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -105,11 +105,17 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): context.data["ftrackEntity"] = asset_entity context.data["ftrackTask"] = task_entity - self.per_instance_process(context, asset_name, task_name) + self.per_instance_process(context, asset_entity, task_entity) def per_instance_process( - self, context, context_asset_name, context_task_name + self, context, context_asset_entity, context_task_entity ): + context_task_name = None + context_asset_name = None + if context_asset_entity: + context_asset_name = context_asset_entity["name"] + if context_task_entity: + context_task_name = context_task_entity["name"] instance_by_asset_and_task = {} for instance in context: self.log.debug( @@ -120,6 +126,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): if not instance_asset_name and not instance_task_name: self.log.debug("Instance does not have set context keys.") + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = context_task_entity continue elif instance_asset_name and instance_task_name: @@ -131,6 +139,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "Instance's context is same as in publish context." " Asset: {} | Task: {}" ).format(context_asset_name, context_task_name)) + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = context_task_entity continue asset_name = instance_asset_name task_name = instance_task_name @@ -141,6 +151,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "Instance's context task is same as in publish" " context. Task: {}" ).format(context_task_name)) + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = context_task_entity continue asset_name = context_asset_name @@ -152,6 +164,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "Instance's context asset is same as in publish" " context. Asset: {}" ).format(context_asset_name)) + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = None continue # Do not use context's task name From 51c27f28c0791633819f935e230a179ea20ff00b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 12:20:06 +0200 Subject: [PATCH 137/155] added ability to add additional metadata to components --- .../publish/integrate_ftrack_instances.py | 134 +++++++++++------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index a1e5922730..3f0cc176a2 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -3,6 +3,7 @@ import json import copy import pyblish.api +from openpype.lib.openpype_version import get_openpype_version from openpype.lib.transcoding import ( get_ffprobe_streams, convert_ffprobe_fps_to_float, @@ -20,6 +21,17 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): label = "Integrate Ftrack Component" families = ["ftrack"] + metadata_keys_to_label = { + "openpype_version": "OpenPype version", + "frame_start": "Frame start", + "frame_end": "Frame end", + "duration": "Duration", + "width": "Resolution width", + "height": "Resolution height", + "fps": "FPS", + "code": "Codec" + } + family_mapping = { "camera": "cam", "look": "look", @@ -43,6 +55,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): } keep_first_subset_name_for_review = True asset_versions_status_profiles = {} + additional_metadata_keys = [] def process(self, instance): self.log.debug("instance {}".format(instance)) @@ -105,7 +118,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "component_data": None, "component_path": None, "component_location": None, - "component_location_name": None + "component_location_name": None, + "additional_data": {} } # Filter types of representations @@ -152,6 +166,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "name": "thumbnail" } thumbnail_item["thumbnail"] = True + # Create copy of item before setting location src_components_to_add.append(copy.deepcopy(thumbnail_item)) # Create copy of first thumbnail @@ -248,19 +263,15 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): first_thumbnail_component[ "asset_data"]["name"] = extended_asset_name - component_meta = self._prepare_component_metadata( - instance, repre, repre_path, True - ) - # Change location review_item["component_path"] = repre_path # Change component data review_item["component_data"] = { # Default component name is "main". "name": "ftrackreview-mp4", - "metadata": { - "ftr_meta": json.dumps(component_meta) - } + "metadata": self._prepare_component_metadata( + instance, repre, repre_path, True + ) } if is_first_review_repre: @@ -302,13 +313,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): component_data = copy_src_item["component_data"] component_name = component_data["name"] component_data["name"] = component_name + "_src" - component_meta = self._prepare_component_metadata( + component_data["metadata"] = self._prepare_component_metadata( instance, repre, copy_src_item["component_path"], False ) - if component_meta: - component_data["metadata"] = { - "ftr_meta": json.dumps(component_meta) - } component_list.append(copy_src_item) # Add others representations as component @@ -326,16 +333,12 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ): other_item["asset_data"]["name"] = extended_asset_name - component_meta = self._prepare_component_metadata( - instance, repre, published_path, False - ) component_data = { - "name": repre["name"] + "name": repre["name"], + "metadata": self._prepare_component_metadata( + instance, repre, published_path, False + ) } - if component_meta: - component_data["metadata"] = { - "ftr_meta": json.dumps(component_meta) - } other_item["component_data"] = component_data other_item["component_location_name"] = unmanaged_location_name other_item["component_path"] = published_path @@ -354,6 +357,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): )) instance.data["ftrackComponentsList"] = component_list + def _collect_additional_metadata(self, streams): + pass + def _get_repre_path(self, instance, repre, only_published): """Get representation path that can be used for integration. @@ -423,6 +429,11 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): def _prepare_component_metadata( self, instance, repre, component_path, is_review ): + metadata = {} + if "openpype_version" in self.additional_metadata_keys: + label = self.metadata_keys_to_label["openpype_version"] + metadata[label] = get_openpype_version() + extension = os.path.splitext(component_path)[-1] streams = [] try: @@ -442,13 +453,23 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # - exr is special case which can have issues with reading through # ffmpegh but we want to set fps for it if not video_streams and extension not in [".exr"]: - return {} + return metadata stream_width = None stream_height = None stream_fps = None frame_out = None + codec_label = None for video_stream in video_streams: + codec_label = video_stream.get("codec_long_name") + if not codec_label: + codec_label = video_stream.get("codec") + + if codec_label: + pix_fmt = video_stream.get("pix_fmt") + if pix_fmt: + codec_label += " ({})".format(pix_fmt) + tmp_width = video_stream.get("width") tmp_height = video_stream.get("height") if tmp_width and tmp_height: @@ -456,8 +477,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): stream_height = tmp_height input_framerate = video_stream.get("r_frame_rate") - duration = video_stream.get("duration") - if input_framerate is None or duration is None: + stream_duration = video_stream.get("duration") + if input_framerate is None or stream_duration is None: continue try: stream_fps = convert_ffprobe_fps_to_float( @@ -473,9 +494,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): stream_height = tmp_height self.log.debug("FPS from stream is {} and duration is {}".format( - input_framerate, duration + input_framerate, stream_duration )) - frame_out = float(duration) * stream_fps + frame_out = float(stream_duration) * stream_fps break # Prepare FPS @@ -483,43 +504,58 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): if instance_fps is None: instance_fps = instance.context.data["fps"] - if not is_review: - output = {} - fps = stream_fps or instance_fps - if fps: - output["frameRate"] = fps - - if stream_width and stream_height: - output["width"] = int(stream_width) - output["height"] = int(stream_height) - return output - - frame_start = repre.get("frameStartFtrack") - frame_end = repre.get("frameEndFtrack") - if frame_start is None or frame_end is None: - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - - fps = None repre_fps = repre.get("fps") if repre_fps is not None: repre_fps = float(repre_fps) fps = stream_fps or repre_fps or instance_fps + # Prepare frame ranges + frame_start = repre.get("frameStartFtrack") + frame_end = repre.get("frameEndFtrack") + if frame_start is None or frame_end is None: + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + duration = (frame_end - frame_start) + 1 + + for key, value in [ + ("fps", fps), + ("frame_start", frame_start), + ("frame_end", frame_end), + ("duration", duration), + ("width", stream_width), + ("height", stream_height), + ("fps", fps), + ("code", codec_label) + ]: + if not value or key not in self.additional_metadata_keys: + continue + label = self.metadata_keys_to_label[key] + metadata[label] = value + + if not is_review: + ftr_meta = {} + if fps: + ftr_meta["frameRate"] = fps + + if stream_width and stream_height: + ftr_meta["width"] = int(stream_width) + ftr_meta["height"] = int(stream_height) + metadata["ftr_meta"] = json.dumps(ftr_meta) + return metadata + # Frame end of uploaded video file should be duration in frames # - frame start is always 0 # - frame end is duration in frames if not frame_out: - frame_out = frame_end - frame_start + 1 + frame_out = duration # Ftrack documentation says that it is required to have # 'width' and 'height' in review component. But with those values # review video does not play. - component_meta = { + metadata["ftr_meta"] = json.dumps({ "frameIn": 0, "frameOut": frame_out, "frameRate": float(fps) - } - - return component_meta + }) + return metadata From b66c8088c3c9fcde06cdcf6cb837c1deb2c5cc1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 12:24:01 +0200 Subject: [PATCH 138/155] added settings for 'additional_metadata_keys' --- .../defaults/project_settings/ftrack.json | 3 ++- .../projects_schema/schema_project_ftrack.json | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 9847e58cfa..952657251c 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -491,7 +491,8 @@ "usd": "usd" }, "keep_first_subset_name_for_review": true, - "asset_versions_status_profiles": [] + "asset_versions_status_profiles": [], + "additional_metadata_keys": [] } } } \ No newline at end of file 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 3f472c6c6a..1a63e589b2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1039,6 +1039,22 @@ } ] } + }, + { + "key": "additional_metadata_keys", + "label": "Additional metadata keys on components", + "type": "enum", + "multiselection": true, + "enum_items": [ + {"openpype_version": "OpenPype version"}, + {"frame_start": "Frame start"}, + {"frame_end": "Frame end"}, + {"duration": "Duration"}, + {"width": "Resolution width"}, + {"height": "Resolution height"}, + {"fps": "FPS"}, + {"code": "Codec"} + ] } ] } From 05f1b732b6edd1732139350528ad614095da5b70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:22:34 +0200 Subject: [PATCH 139/155] fill context task entity in collect ftrack api --- openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index 99a555014e..e13b7e65cd 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -165,7 +165,7 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): " context. Asset: {}" ).format(context_asset_name)) instance.data["ftrackEntity"] = context_asset_entity - instance.data["ftrackTask"] = None + instance.data["ftrackTask"] = context_task_entity continue # Do not use context's task name From 4dba68c5bdade98048dd1ca15d7f03ac004dcf28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:30:14 +0200 Subject: [PATCH 140/155] fix function import and call --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 24f784f83d..0a7ad0b532 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -1,5 +1,5 @@ import pyblish.api -from openpype.lib import profiles_filtering +from openpype.lib import filter_profiles class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): @@ -63,7 +63,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): host_name = context.data["hostName"] task_name = task_entity["name"] task_type = task_entity["type"]["name"] - status_profile = profiles_filtering( + status_profile = filter_profiles( self.farm_status_profiles, { "hosts": host_name, @@ -75,7 +75,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): logger=self.log ) if not status_profile: - # There already is log in 'profiles_filtering' + # There already is log in 'filter_profiles' continue status_name = status_profile["status_name"] From cb34f4619e54ae887bc1ea38a0e1ec106d228167 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:30:20 +0200 Subject: [PATCH 141/155] log availabl status names --- .../plugins/publish/integrate_ftrack_farm_status.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 0a7ad0b532..8bebfd8485 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -93,6 +93,10 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): project_schema = project_entity["project_schema"] task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + joined_status_names = ", ".join({ + '"{}"'.format(status["name"]) + for status in task_workflow_statuses + }) # Keep track if anything has changed status_changed = False found_status_id_by_status_name = {} @@ -117,8 +121,9 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): if status_id is None: self.log.warning(( - "Status \"{}\" is not available on project \"{}\"" - ).format(status_name, project_name)) + "Status \"{}\" is not available on project \"{}\"." + " Available statuses are {}" + ).format(status_name, project_name, joined_status_names)) continue # Change task status id From bc3aa4b1609e067e7b4a31a9874e8a415bcfcc71 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:54:06 +0200 Subject: [PATCH 142/155] fix getting of task statuses --- .../plugins/publish/integrate_ftrack_farm_status.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 8bebfd8485..658df70895 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -90,12 +90,17 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): project_entity = session.query(( "select project_schema from Project where full_name is \"{}\"" ).format(project_name)).one() + task_type = session.query( + "select id from ObjectType where name is \"Task\"" + ).first() project_schema = project_entity["project_schema"] - task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + task_statuses = project_schema.get_statuses( + "Task", task_type["id"] + ) joined_status_names = ", ".join({ '"{}"'.format(status["name"]) - for status in task_workflow_statuses + for status in task_statuses }) # Keep track if anything has changed status_changed = False @@ -111,7 +116,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): if status_name_low in found_status_id_by_status_name: continue - for status in task_workflow_statuses: + for status in task_statuses: if status["name"].lower() == status_name_low: status_id = status["id"] break From 671cf183fd73629e7a140784e518bc7718fa5431 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 14:18:13 +0200 Subject: [PATCH 143/155] fix statuses lookup by task type --- .../publish/integrate_ftrack_farm_status.py | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 658df70895..c5fc3dd68f 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -90,49 +90,49 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): project_entity = session.query(( "select project_schema from Project where full_name is \"{}\"" ).format(project_name)).one() - task_type = session.query( - "select id from ObjectType where name is \"Task\"" - ).first() project_schema = project_entity["project_schema"] - task_statuses = project_schema.get_statuses( - "Task", task_type["id"] - ) - joined_status_names = ", ".join({ - '"{}"'.format(status["name"]) - for status in task_statuses - }) + task_type_ids = set() + for item in instances_with_status_names: + instance, _ = item + task_entity = instance.data["ftrackTask"] + task_type_ids.add(task_entity["type"]["id"]) + + task_statuses_by_type_id = { + task_type_id: project_schema.get_statuses("Task", task_type_id) + for task_type_id in task_type_ids + } + # Keep track if anything has changed + skipped_status_names = set() status_changed = False - found_status_id_by_status_name = {} for item in instances_with_status_names: instance, status_name = item - + task_entity = instance.data["ftrackTask"] + task_statuses = task_statuses_by_type_id[task_entity["type"]["id"]] status_name_low = status_name.lower() - status_id = found_status_id_by_status_name.get(status_name_low) + + status_id = None + # Skip if status name was already tried to be found + for status in task_statuses: + if status["name"].lower() == status_name_low: + status_id = status["id"] + break if status_id is None: - # Skip if status name was already tried to be found - if status_name_low in found_status_id_by_status_name: - continue - - for status in task_statuses: - if status["name"].lower() == status_name_low: - status_id = status["id"] - break - - # Store the result to be reused in following instances - found_status_id_by_status_name[status_name_low] = status_id - - if status_id is None: - self.log.warning(( - "Status \"{}\" is not available on project \"{}\"." - " Available statuses are {}" - ).format(status_name, project_name, joined_status_names)) + if status_name_low not in skipped_status_names: + skipped_status_names.add(status_name_low) + joined_status_names = ", ".join({ + '"{}"'.format(status["name"]) + for status in task_statuses + }) + self.log.warning(( + "Status \"{}\" is not available on project \"{}\"." + " Available statuses are {}" + ).format(status_name, project_name, joined_status_names)) continue # Change task status id - task_entity = instance.data["ftrackTask"] if status_id != task_entity["status_id"]: task_entity["status_id"] = status_id status_changed = True From 6d4a80cd30b8adf926a44b46c2c8f70ee04217f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 14:28:27 +0200 Subject: [PATCH 144/155] added some logs related to status changes --- .../plugins/publish/integrate_ftrack_farm_status.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index c5fc3dd68f..ab5738c33f 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -113,10 +113,12 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): status_name_low = status_name.lower() status_id = None + status_name = None # Skip if status name was already tried to be found for status in task_statuses: if status["name"].lower() == status_name_low: status_id = status["id"] + status_name = status["name"] break if status_id is None: @@ -136,6 +138,13 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): if status_id != task_entity["status_id"]: task_entity["status_id"] = status_id status_changed = True + path = "/".join([ + item["name"] + for item in task_entity["link"] + ]) + self.log.debug("Set status \"{}\" to \"{}\"".format( + status_name, path + )) if status_changed: session.commit() From 5a0b15c63b90a97417d43d9a3cfff7ba927dd4e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 15:35:46 +0200 Subject: [PATCH 145/155] fix typo in codec --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 3f0cc176a2..1bf4caac77 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -29,7 +29,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "width": "Resolution width", "height": "Resolution height", "fps": "FPS", - "code": "Codec" + "codec": "Codec" } family_mapping = { @@ -526,7 +526,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ("width", stream_width), ("height", stream_height), ("fps", fps), - ("code", codec_label) + ("codec", codec_label) ]: if not value or key not in self.additional_metadata_keys: continue From 09af23e2d789dcb02450cbd6eed9be53c062c416 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Aug 2022 17:26:10 +0200 Subject: [PATCH 146/155] resolve: fixing import in collector --- .../hosts/resolve/plugins/publish/precollect_workfile.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index 53e67aee0e..0f94216556 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -1,11 +1,9 @@ import pyblish.api from pprint import pformat -from importlib import reload -from openpype.hosts import resolve +from openpype.hosts.resolve import api as rapi from openpype.pipeline import legacy_io from openpype.hosts.resolve.otio import davinci_export -reload(davinci_export) class PrecollectWorkfile(pyblish.api.ContextPlugin): @@ -18,9 +16,9 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): asset = legacy_io.Session["AVALON_ASSET"] subset = "workfile" - project = resolve.get_current_project() + project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") - video_tracks = resolve.get_video_track_names() + video_tracks = rapi.get_video_track_names() # adding otio timeline to context otio_timeline = davinci_export.create_otio_timeline(project) From da3268c9a75e04a8464589fc1c1153e264fec60a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 09:51:45 +0200 Subject: [PATCH 147/155] resave default settings --- openpype/settings/defaults/project_settings/ftrack.json | 2 +- openpype/settings/defaults/project_settings/shotgrid.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 58b6a55958..2d5f889aa5 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -498,4 +498,4 @@ "farm_status_profiles": [] } } -} +} \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/shotgrid.json b/openpype/settings/defaults/project_settings/shotgrid.json index 83b6f69074..774bce714b 100644 --- a/openpype/settings/defaults/project_settings/shotgrid.json +++ b/openpype/settings/defaults/project_settings/shotgrid.json @@ -19,4 +19,4 @@ "step": "step" } } -} +} \ No newline at end of file From 2b62f28e903e524e66d9520a86ea21ae3df81283 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:27:13 +0200 Subject: [PATCH 148/155] fix 'get_representations_parents' function to be able handle hero versions --- openpype/client/entities.py | 88 ++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 67ddb09ddb..f1f1d30214 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1259,58 +1259,64 @@ def get_representations_parents(project_name, representations): dict[ObjectId, tuple]: Parents by representation id. """ - repres_by_version_id = collections.defaultdict(list) - versions_by_version_id = {} - versions_by_subset_id = collections.defaultdict(list) - subsets_by_subset_id = {} - subsets_by_asset_id = collections.defaultdict(list) + 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 representation in representations: - repre_id = representation["_id"] + for repre_doc in representations: + repre_id = repre_doc["_id"] + version_id = repre_doc["parent"] output[repre_id] = (None, None, None, None) - version_id = representation["parent"] - repres_by_version_id[version_id].append(representation) + repre_docs_by_version_id[version_id].append(repre_doc) - versions = get_versions( - project_name, version_ids=repres_by_version_id.keys() + version_docs = get_versions( + project_name, + version_ids=repre_docs_by_version_id.keys(), + hero=True ) - for version in versions: - version_id = version["_id"] - subset_id = version["parent"] - versions_by_version_id[version_id] = version - versions_by_subset_id[subset_id].append(version) + 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) - subsets = get_subsets( - project_name, subset_ids=versions_by_subset_id.keys() + subset_docs = get_subsets( + project_name, subset_ids=version_docs_by_subset_id.keys() ) - for subset in subsets: - subset_id = subset["_id"] - asset_id = subset["parent"] - subsets_by_subset_id[subset_id] = subset - subsets_by_asset_id[asset_id].append(subset) + 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) - assets = get_assets(project_name, asset_ids=subsets_by_asset_id.keys()) - assets_by_id = { - asset["_id"]: asset - for asset in assets + 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 = get_project(project_name) + project_doc = get_project(project_name) - for version_id, representations in repres_by_version_id.items(): - asset = None - subset = None - version = versions_by_version_id.get(version_id) - if version: - subset_id = version["parent"] - subset = subsets_by_subset_id.get(subset_id) - if subset: - asset_id = subset["parent"] - asset = assets_by_id.get(asset_id) + 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 representation in representations: - repre_id = representation["_id"] - output[repre_id] = (version, subset, asset, project) + for repre_doc in repre_docs: + repre_id = repre_doc["_id"] + output[repre_id] = ( + version_doc, subset_doc, asset_doc, project_doc + ) return output From ce746737154e5c7b12f9a0da5ef47b0edd911f64 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:28:10 +0200 Subject: [PATCH 149/155] Be explicit in error message what is missing --- openpype/pipeline/load/utils.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 9945e1fce4..99d6876d4b 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -222,13 +222,20 @@ def get_representation_context(representation): project_name, representation ) + if not representation: + raise AssertionError("Representation was not found in database") + version, subset, asset, project = get_representation_parents( project_name, representation ) - - assert all([representation, version, subset, asset, project]), ( - "This is a bug" - ) + if not version: + raise AssertionError("Version was not found in database") + if not subset: + raise AssertionError("Subset was not found in database") + if not asset: + raise AssertionError("Asset was not found in database") + if not project: + raise AssertionError("Project was not found in database") context = { "project": { From 9d54333e93afe14b3686cc429009632cf1f24f00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:28:54 +0200 Subject: [PATCH 150/155] load error can handle invalid hero version --- openpype/tools/loader/widgets.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 2d8b4b048d..597c35e89b 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -1547,6 +1547,11 @@ def _load_representations_by_loader(loader, repre_contexts, return for repre_context in repre_contexts.values(): + version_doc = repre_context["version"] + if version_doc["type"] == "hero_version": + version_name = "Hero" + else: + version_name = version_doc.get("name") try: if data_by_repre_id: _id = repre_context["representation"]["_id"] @@ -1564,7 +1569,7 @@ def _load_representations_by_loader(loader, repre_contexts, None, repre_context["representation"]["name"], repre_context["subset"]["name"], - repre_context["version"]["name"] + version_name )) except Exception as exc: @@ -1577,7 +1582,7 @@ def _load_representations_by_loader(loader, repre_contexts, formatted_traceback, repre_context["representation"]["name"], repre_context["subset"]["name"], - repre_context["version"]["name"] + version_name )) return error_info From ac6de74b76dd741152c11d71c2262f605847acd7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:33:09 +0200 Subject: [PATCH 151/155] handle hero version type in load clip --- openpype/hosts/nuke/plugins/load/load_clip.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index b2dc4a52d7..346773b5af 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -162,7 +162,15 @@ class LoadClip(plugin.NukeLoader): data_imprint = {} for k in add_keys: if k == 'version': - data_imprint[k] = context["version"]['name'] + version_doc = context["version"] + if version_doc["type"] == "hero_version": + version = "hero" + else: + version = version_doc.get("name") + + if version: + data_imprint[k] = version + elif k == 'colorspace': colorspace = repre["data"].get(k) colorspace = colorspace or version_data.get(k) From 70cfa733f3e7e985580ec8fff8520c31ec5184c8 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 18 Aug 2022 18:34:34 +0000 Subject: [PATCH 152/155] [Automated] Bump version --- CHANGELOG.md | 27 +++++++++++++++++++-------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80673e9f8a..b192d26250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,25 @@ # Changelog -## [3.13.1-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...HEAD) +**🆕 New features** + +- Maya: Build workfile by template [\#3578](https://github.com/pypeclub/OpenPype/pull/3578) + +**🚀 Enhancements** + +- Ftrack: Addiotional component metadata [\#3685](https://github.com/pypeclub/OpenPype/pull/3685) +- Ftrack: Set task status on farm publishing [\#3680](https://github.com/pypeclub/OpenPype/pull/3680) +- Ftrack: Set task status on task creation in integrate hierarchy [\#3675](https://github.com/pypeclub/OpenPype/pull/3675) +- Maya: Disable rendering of all lights for render instances submitted through Deadline. [\#3661](https://github.com/pypeclub/OpenPype/pull/3661) +- General: Optimized OCIO configs [\#3650](https://github.com/pypeclub/OpenPype/pull/3650) + **🐛 Bug fixes** +- General: Switch from hero version to versioned works [\#3691](https://github.com/pypeclub/OpenPype/pull/3691) +- General: Fix finding of last version [\#3656](https://github.com/pypeclub/OpenPype/pull/3656) - General: Extract Review can scale with pixel aspect ratio [\#3644](https://github.com/pypeclub/OpenPype/pull/3644) - Maya: Refactor moved usage of CreateRender settings [\#3643](https://github.com/pypeclub/OpenPype/pull/3643) - General: Hero version representations have full context [\#3638](https://github.com/pypeclub/OpenPype/pull/3638) @@ -14,8 +28,12 @@ **🔀 Refactored code** +- General: Use client projects getter [\#3673](https://github.com/pypeclub/OpenPype/pull/3673) +- Resolve: Match folder structure to other hosts [\#3653](https://github.com/pypeclub/OpenPype/pull/3653) +- Maya: Hosts as modules [\#3647](https://github.com/pypeclub/OpenPype/pull/3647) - TimersManager: Plugins are in timers manager module [\#3639](https://github.com/pypeclub/OpenPype/pull/3639) - General: Move workfiles functions into pipeline [\#3637](https://github.com/pypeclub/OpenPype/pull/3637) +- General: Workfiles builder using query functions [\#3598](https://github.com/pypeclub/OpenPype/pull/3598) **Merged pull requests:** @@ -89,7 +107,6 @@ **🚀 Enhancements** - General: Global thumbnail extractor is ready for more cases [\#3561](https://github.com/pypeclub/OpenPype/pull/3561) -- Maya: add additional validators to Settings [\#3540](https://github.com/pypeclub/OpenPype/pull/3540) **🐛 Bug fixes** @@ -100,16 +117,10 @@ - General: Remove hosts filter on integrator plugins [\#3556](https://github.com/pypeclub/OpenPype/pull/3556) - Settings: Clean default values of environments [\#3550](https://github.com/pypeclub/OpenPype/pull/3550) - Module interfaces: Fix import error [\#3547](https://github.com/pypeclub/OpenPype/pull/3547) -- Workfiles tool: Show of tool and it's flags [\#3539](https://github.com/pypeclub/OpenPype/pull/3539) -- General: Create workfile documents works again [\#3538](https://github.com/pypeclub/OpenPype/pull/3538) **🔀 Refactored code** - General: Use query functions in integrator [\#3563](https://github.com/pypeclub/OpenPype/pull/3563) -- General: Mongo core connection moved to client [\#3531](https://github.com/pypeclub/OpenPype/pull/3531) -- Refactor Integrate Asset [\#3530](https://github.com/pypeclub/OpenPype/pull/3530) -- General: Client docstrings cleanup [\#3529](https://github.com/pypeclub/OpenPype/pull/3529) -- General: Move load related functions into pipeline [\#3527](https://github.com/pypeclub/OpenPype/pull/3527) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 9ae52e8370..38723ed123 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.13.1-nightly.3" +__version__ = "3.14.0-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 287a3c78f0..4d4aff01a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.13.1-nightly.3" # OpenPype +version = "3.14.0-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 9f879bb22a2a01fe17adc1b7e9e61df8603e6537 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 18 Aug 2022 18:47:09 +0000 Subject: [PATCH 153/155] [Automated] Release --- CHANGELOG.md | 6 +++--- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b192d26250..e19993ad75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.14.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.0](https://github.com/pypeclub/OpenPype/tree/3.14.0) (2022-08-18) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...3.14.0) **🆕 New features** @@ -25,6 +25,7 @@ - General: Hero version representations have full context [\#3638](https://github.com/pypeclub/OpenPype/pull/3638) - Nuke: color settings for render write node is working now [\#3632](https://github.com/pypeclub/OpenPype/pull/3632) - Maya: FBX support for update in reference loader [\#3631](https://github.com/pypeclub/OpenPype/pull/3631) +- Integrator: Don't force to have dot before frame [\#3611](https://github.com/pypeclub/OpenPype/pull/3611) **🔀 Refactored code** @@ -69,7 +70,6 @@ - Ftrack: Sync hierarchical attributes can handle new created entities [\#3621](https://github.com/pypeclub/OpenPype/pull/3621) - General: Extract review aspect ratio scale is calculated by ffmpeg [\#3620](https://github.com/pypeclub/OpenPype/pull/3620) - Maya: Fix types of default settings [\#3617](https://github.com/pypeclub/OpenPype/pull/3617) -- Integrator: Don't force to have dot before frame [\#3611](https://github.com/pypeclub/OpenPype/pull/3611) - AfterEffects: refactored integrate doesnt work formulti frame publishes [\#3610](https://github.com/pypeclub/OpenPype/pull/3610) - Maya look data contents fails with custom attribute on group [\#3607](https://github.com/pypeclub/OpenPype/pull/3607) - TrayPublisher: Fix wrong conflict merge [\#3600](https://github.com/pypeclub/OpenPype/pull/3600) diff --git a/openpype/version.py b/openpype/version.py index 38723ed123..c28b480940 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.0-nightly.1" +__version__ = "3.14.0" diff --git a/pyproject.toml b/pyproject.toml index 4d4aff01a2..e670d0a2ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.14.0-nightly.1" # OpenPype +version = "3.14.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 496728e9abc54dbce957127e8e8134f629ed3ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 19 Aug 2022 12:09:52 +0200 Subject: [PATCH 154/155] :recycle: handle host name that is not set --- openpype/plugins/publish/extract_review.py | 2 ++ openpype/plugins/publish/integrate_subset_group.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e16f324e0a..27117510b2 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1459,6 +1459,8 @@ class ExtractReview(pyblish.api.InstancePlugin): output = -1 regexes = self.compile_list_of_regexes(in_list) for regex in regexes: + if not value: + continue if re.match(regex, value): output = 1 break diff --git a/openpype/plugins/publish/integrate_subset_group.py b/openpype/plugins/publish/integrate_subset_group.py index 910cb060a6..79dd10fb8f 100644 --- a/openpype/plugins/publish/integrate_subset_group.py +++ b/openpype/plugins/publish/integrate_subset_group.py @@ -93,6 +93,6 @@ class IntegrateSubsetGroup(pyblish.api.InstancePlugin): return { "families": anatomy_data["family"], "tasks": task.get("name"), - "hosts": anatomy_data["app"], + "hosts": anatomy_data.get("app"), "task_types": task.get("type") } From 8cd15708b65213092924263b0386f8bec28dc7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 19 Aug 2022 13:07:52 +0200 Subject: [PATCH 155/155] :bug: use the right key Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/publish/integrate_subset_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_subset_group.py b/openpype/plugins/publish/integrate_subset_group.py index 79dd10fb8f..a24ebba3a5 100644 --- a/openpype/plugins/publish/integrate_subset_group.py +++ b/openpype/plugins/publish/integrate_subset_group.py @@ -93,6 +93,6 @@ class IntegrateSubsetGroup(pyblish.api.InstancePlugin): return { "families": anatomy_data["family"], "tasks": task.get("name"), - "hosts": anatomy_data.get("app"), + "hosts": instance.context.data["hostName"], "task_types": task.get("type") }