Skip to content

Commit

Permalink
Merge pull request #60 from quadproduction/release/4.1.3
Browse files Browse the repository at this point in the history
Release/4.1.3
  • Loading branch information
BenSouchet authored Feb 19, 2025
2 parents 56621ad + 63fb198 commit 6fc8fb4
Show file tree
Hide file tree
Showing 20 changed files with 602 additions and 72 deletions.
5 changes: 2 additions & 3 deletions src/igniter/zxp_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
import semver
from qtpy import QtCore

from .version_classes import PackageVersion


class ZXPExtensionData:

Expand Down Expand Up @@ -91,7 +89,8 @@ def get_zxp_extensions_to_update(running_version_fullpath, global_settings, forc
return []
elif low_platform == "darwin":
# TODO: implement this function for macOS
raise NotImplementedError(f"MacOS not implemented, implementation need before the first macOS release")
return []
# raise NotImplementedError(f"MacOS not implemented, implementation need before the first macOS release")

zxp_host_ids = ["photoshop", "aftereffects"]

Expand Down
11 changes: 9 additions & 2 deletions src/quadpype/hosts/blender/api/template_resolving.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
from quadpype.settings import get_project_settings
from quadpype.lib import (
filter_profiles,
Logger,
StringTemplate,
)


def get_resolved_name(data, template):
"""Resolve template_collections_naming with entered data.
Args:
data (Dict[str, Any]): Data to fill template_collections_naming.
template (list): template to solve
template (str): template to solve
Returns:
str: Resolved template
"""
Expand All @@ -20,6 +20,7 @@ def get_resolved_name(data, template):
output = template_obj.format_strict(data)
return output.normalized()


def _get_project_name_by_data(data):
"""
Retrieve the project name depending on given data
Expand All @@ -39,6 +40,7 @@ def _get_project_name_by_data(data):

return project_name, is_from_anatomy


def _get_app_name_by_data(data):
"""
Retrieve the app name depending on given data
Expand All @@ -58,6 +60,7 @@ def _get_app_name_by_data(data):

return app_name, is_from_anatomy


def _get_parent_by_data(data):
"""
Retrieve the parent asset name depending on given data
Expand All @@ -77,6 +80,7 @@ def _get_parent_by_data(data):

return parent_name, is_from_anatomy


def _get_profiles(setting_key, data, project_settings=None):

project_name, is_anatomy_data = _get_project_name_by_data(data)
Expand Down Expand Up @@ -104,6 +108,7 @@ def _get_profiles(setting_key, data, project_settings=None):

return profiles


def _get_entity_prefix(data):
"""Retrieve the asset_type (entity_type) short name for proper blender naming
Args:
Expand All @@ -122,6 +127,7 @@ def _get_entity_prefix(data):
# If a profile is found, return the prefix
return profile.get("entity_prefix"), is_anatomy


def update_parent_data_with_entity_prefix(data):
"""
Will update the input data dict to change the value of the ["parent"] key
Expand All @@ -139,6 +145,7 @@ def update_parent_data_with_entity_prefix(data):
else:
data["parent"] = parent_prefix


def get_entity_collection_template(data):
"""Retrieve the template for the collection depending on the entity type
Args:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import bpy
import inspect
from pathlib import Path
import pyblish.api

from quadpype.pipeline import (
OptionalPyblishPluginMixin,
Anatomy
)

from quadpype.hosts.blender.api import (
get_resolved_name
)
from quadpype.pipeline.publish import (
RepairContextAction,
ValidateContentsOrder,
PublishValidationError
)

class ValidateDataBlockRootPaths(pyblish.api.ContextPlugin,
OptionalPyblishPluginMixin):
"""Validates Data Block Paths are in any given root path
This validator checks if all external data paths are from
one of the given root path in settings
"""

label = "Validate Data Block Paths Location"
order = ValidateContentsOrder
hosts = ["blender"]
exclude_families = []
optional = True
root_paths = list()

@classmethod
def get_invalid(cls, context):
"""Get all invalid data block path if not in any root paths"""
invalid = []
object_type = type(bpy.data.objects)
for attr in dir(bpy.data):
collections = getattr(bpy.data, attr)
if not isinstance(collections, object_type):
continue
for data_block in collections:
if not hasattr(data_block, "filepath"):
continue
if not data_block.filepath:
continue

path = Path(bpy.path.abspath(data_block.filepath))
path_full = path.resolve()

if any(path_full.is_relative_to(root_path) for root_path in cls.root_paths):
continue

cls.log.warning(f"Data Block {attr} filepath {data_block.filepath} "
"is not in a root path")
invalid.append(data_block)
return invalid

@classmethod
def get_root_paths(cls, context):
"""Retrieve and solve all roots from Anatomy() with context data
Will create a list of pathlib.Path"""
anatomy = Anatomy()
for root_name, root_val in anatomy.roots.items():
resolved_path = get_resolved_name(context.data.get('anatomyData', {}), str(root_val))
cls.root_paths.append(Path(resolved_path).resolve())

def process(self, context):
if not self.is_active(context.data):
self.log.debug("Skipping Validate Data Block Paths Location...")
return

# Generate the root Paths
self.get_root_paths(context)

invalid = self.get_invalid(context)

if invalid:
invalid_msg = f"\n-\n".join(bpy.path.abspath(image.filepath) for image in invalid)
root_path_msg = "\n".join(path.as_posix() for path in self.root_paths)
raise PublishValidationError(
f"DataBlock filepath are not in any roots:\n"
f"{invalid_msg}\n"
f"--------------------------------------\n"
f"Validate roots paths are:\n"
f"{root_path_msg}",
title="Invalid Image Path",
description=self.get_description()
)

@classmethod
def get_description(cls):
return inspect.cleandoc("""
### DataBlock filepaths are invalid
Data Block filepaths must be in any of the root paths.
""")
38 changes: 27 additions & 11 deletions src/quadpype/hosts/tvpaint/api/communication_server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
import json
import time
import subprocess
Expand All @@ -12,8 +13,11 @@
import threading
import shutil

from pathlib import Path
from contextlib import closing

import semver

from aiohttp import web
from aiohttp_json_rpc import JsonRpc
from aiohttp_json_rpc.protocol import (
Expand Down Expand Up @@ -540,6 +544,18 @@ def _windows_file_process(self, src_dst_mapping, to_remove):
# Remove temp folder
shutil.rmtree(tmp_dir)

@staticmethod
def _get_host_version(executable_filename):
version_dict = {'major': 0, 'minor': 0, 'patch': 0}
regex_match = re.search(r"([\d.]+)", executable_filename)
version_elem_list = regex_match.group(1).split(".")
for index, version_elem_key in enumerate(version_dict):
if index >= len(version_elem_list):
break
version_dict[version_elem_key] = int(version_elem_list[index])

return version_dict

def _prepare_windows_plugin(self, launch_args):
"""Copy plugin to TVPaint plugins and set PATH to dependencies.
Expand All @@ -549,17 +565,14 @@ def _prepare_windows_plugin(self, launch_args):
to PATH variable.
"""

host_executable = launch_args[0]
executable_file = os.path.basename(host_executable)
host_executable = Path(launch_args[0])
executable_file = host_executable.name

subfolder = "windows_x64"
if "64bit" in executable_file:
subfolder = "windows_x64"
elif "32bit" in executable_file:
subfolder = "windows_x86"
else:
raise ValueError(
"Can't determine if executable "
"leads to 32-bit or 64-bit TVPaint!"
)

plugin_files_path = get_plugin_files_path()
# Folder for right windows plugin files
Expand All @@ -580,10 +593,13 @@ def _prepare_windows_plugin(self, launch_args):
os.environ["PATH"] += (os.pathsep + additional_libs_folder)

# Path to TVPaint's plugins folder (where we want to add our plugin)
host_plugins_path = os.path.join(
os.path.dirname(host_executable),
"plugins"
)
host_exe_dir = host_executable.parent
host_exe_version = self._get_host_version(executable_file)

if host_exe_version >= semver.VersionInfo(major=12, minor=0, patch=0):
host_plugins_path = host_exe_dir.joinpath("Resources", "plugins")
else:
host_plugins_path = host_exe_dir.joinpath("plugins")

# Files that must be copied to TVPaint's plugin folder
plugin_dir = os.path.join(source_plugins_dir, "plugin")
Expand Down
28 changes: 24 additions & 4 deletions src/quadpype/lib/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import semver
import requests
from requests.adapters import HTTPAdapter, Retry
from htmllistparse import fetch_listing

from pathlib import Path
Expand All @@ -17,6 +18,11 @@

ADDONS_SETTINGS_KEY = "addons"
_NOT_SET = object()
HTTP_ADAPTER = HTTPAdapter(max_retries=Retry(
total=3,
backoff_factor=0.2,
status_forcelist=[500, 502, 503, 504]
))

# Versions should match any string complying with https://semver.org/
VERSION_REGEX = re.compile(r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>[a-zA-Z\d\-.]*))?(?:\+(?P<buildmetadata>[a-zA-Z\d\-.]*))?$") # noqa: E501
Expand Down Expand Up @@ -201,6 +207,9 @@ def _extract_member(self, member, target_path, pwd):
class PackageHandler:
"""Class for handling a package."""
type = "package"
_request_session = requests.Session()
_request_session.mount('http://', HTTP_ADAPTER)
_request_session.mount('https://', HTTP_ADAPTER)

def __init__(self,
pkg_name: str,
Expand Down Expand Up @@ -378,7 +387,7 @@ def get_accessible_remote_source(self):
return remote_source
elif isinstance(remote_source, SourceURL):
try:
response = requests.head(remote_source)
response = self._request_session.get(remote_source)
if response.ok:
return remote_source
except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError):
Expand Down Expand Up @@ -637,7 +646,11 @@ def get_versions_from_url(cls, pkg_name: str, source_url: SourceURL, priority_to
if not source_url:
return versions

response = requests.head(source_url)
try:
response = cls._request_session.head(source_url)
except Exception: # noqa
return versions

page_content_type = "text/html"
allowed_content_type = [
"application/zip",
Expand Down Expand Up @@ -673,7 +686,11 @@ def get_versions_from_url(cls, pkg_name: str, source_url: SourceURL, priority_to

continue

response = requests.head(item_full_url)
try:
response = cls._request_session.head(item_full_url)
except Exception: # noqa
continue

if response.status_code != 200 or response.headers.get("content-type") not in allowed_content_type:
continue

Expand Down Expand Up @@ -807,7 +824,10 @@ def find_version(self, version: Union[PackageVersion, str], from_local: bool = F

@staticmethod
def _download_version(remote_version: PackageVersion, dest_archive_path: Path):
response = requests.get(remote_version.location, stream=True)
try:
response = PackageHandler._request_session.get(remote_version.location, stream=True)
except Exception as e: # noqa
raise Exception(f"Failed to download {remote_version.location}. Error: {e}")

if response.status_code == 200:
with open(dest_archive_path, "wb") as file:
Expand Down
12 changes: 6 additions & 6 deletions src/quadpype/pipeline/anatomy.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def root_environmets_fill_data(self, template=None):
template (str): Template for environment variable key fill.
By default is set to `"${}"`.
"""
return self.roots_obj.root_environmets_fill_data(template)
return self.roots_obj.root_environments_fill_data(template)

def find_root_template_from_path(self, *args, **kwargs):
"""Wrapper for Roots `find_root_template_from_path`."""
Expand Down Expand Up @@ -1400,7 +1400,7 @@ def _root_environments(self, keys=None, roots=None):
output.update(self._root_environments(_keys, _value))
return output

def root_environmets_fill_data(self, template=None):
def root_environments_fill_data(self, template=None):
"""Environment variable values in dictionary for rootless path.
Args:
Expand All @@ -1409,12 +1409,12 @@ def root_environmets_fill_data(self, template=None):
"""
if template is None:
template = "${}"
return self._root_environmets_fill_data(template)
return self._root_environments_fill_data(template)

def _root_environmets_fill_data(self, template, keys=None, roots=None):
def _root_environments_fill_data(self, template, keys=None, roots=None):
if keys is None and roots is None:
return {
"root": self._root_environmets_fill_data(
"root": self._root_environments_fill_data(
template, [], self.roots
)
}
Expand All @@ -1430,7 +1430,7 @@ def _root_environmets_fill_data(self, template, keys=None, roots=None):
for key, value in roots.items():
_keys = list(keys)
_keys.append(key)
output[key] = self._root_environmets_fill_data(
output[key] = self._root_environments_fill_data(
template, _keys, value
)
return output
Expand Down
Loading

0 comments on commit 6fc8fb4

Please sign in to comment.