Skip to content

Commit 6fc8fb4

Browse files
authored
Merge pull request #60 from quadproduction/release/4.1.3
Release/4.1.3
2 parents 56621ad + 63fb198 commit 6fc8fb4

20 files changed

+602
-72
lines changed

src/igniter/zxp_utils.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import semver
1111
from qtpy import QtCore
1212

13-
from .version_classes import PackageVersion
14-
1513

1614
class ZXPExtensionData:
1715

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

9695
zxp_host_ids = ["photoshop", "aftereffects"]
9796

src/quadpype/hosts/blender/api/template_resolving.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
from quadpype.settings import get_project_settings
44
from quadpype.lib import (
55
filter_profiles,
6-
Logger,
76
StringTemplate,
87
)
98

9+
1010
def get_resolved_name(data, template):
1111
"""Resolve template_collections_naming with entered data.
1212
Args:
1313
data (Dict[str, Any]): Data to fill template_collections_naming.
14-
template (list): template to solve
14+
template (str): template to solve
1515
Returns:
1616
str: Resolved template
1717
"""
@@ -20,6 +20,7 @@ def get_resolved_name(data, template):
2020
output = template_obj.format_strict(data)
2121
return output.normalized()
2222

23+
2324
def _get_project_name_by_data(data):
2425
"""
2526
Retrieve the project name depending on given data
@@ -39,6 +40,7 @@ def _get_project_name_by_data(data):
3940

4041
return project_name, is_from_anatomy
4142

43+
4244
def _get_app_name_by_data(data):
4345
"""
4446
Retrieve the app name depending on given data
@@ -58,6 +60,7 @@ def _get_app_name_by_data(data):
5860

5961
return app_name, is_from_anatomy
6062

63+
6164
def _get_parent_by_data(data):
6265
"""
6366
Retrieve the parent asset name depending on given data
@@ -77,6 +80,7 @@ def _get_parent_by_data(data):
7780

7881
return parent_name, is_from_anatomy
7982

83+
8084
def _get_profiles(setting_key, data, project_settings=None):
8185

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

105109
return profiles
106110

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

130+
125131
def update_parent_data_with_entity_prefix(data):
126132
"""
127133
Will update the input data dict to change the value of the ["parent"] key
@@ -139,6 +145,7 @@ def update_parent_data_with_entity_prefix(data):
139145
else:
140146
data["parent"] = parent_prefix
141147

148+
142149
def get_entity_collection_template(data):
143150
"""Retrieve the template for the collection depending on the entity type
144151
Args:
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import bpy
2+
import inspect
3+
from pathlib import Path
4+
import pyblish.api
5+
6+
from quadpype.pipeline import (
7+
OptionalPyblishPluginMixin,
8+
Anatomy
9+
)
10+
11+
from quadpype.hosts.blender.api import (
12+
get_resolved_name
13+
)
14+
from quadpype.pipeline.publish import (
15+
RepairContextAction,
16+
ValidateContentsOrder,
17+
PublishValidationError
18+
)
19+
20+
class ValidateDataBlockRootPaths(pyblish.api.ContextPlugin,
21+
OptionalPyblishPluginMixin):
22+
"""Validates Data Block Paths are in any given root path
23+
24+
This validator checks if all external data paths are from
25+
one of the given root path in settings
26+
"""
27+
28+
label = "Validate Data Block Paths Location"
29+
order = ValidateContentsOrder
30+
hosts = ["blender"]
31+
exclude_families = []
32+
optional = True
33+
root_paths = list()
34+
35+
@classmethod
36+
def get_invalid(cls, context):
37+
"""Get all invalid data block path if not in any root paths"""
38+
invalid = []
39+
object_type = type(bpy.data.objects)
40+
for attr in dir(bpy.data):
41+
collections = getattr(bpy.data, attr)
42+
if not isinstance(collections, object_type):
43+
continue
44+
for data_block in collections:
45+
if not hasattr(data_block, "filepath"):
46+
continue
47+
if not data_block.filepath:
48+
continue
49+
50+
path = Path(bpy.path.abspath(data_block.filepath))
51+
path_full = path.resolve()
52+
53+
if any(path_full.is_relative_to(root_path) for root_path in cls.root_paths):
54+
continue
55+
56+
cls.log.warning(f"Data Block {attr} filepath {data_block.filepath} "
57+
"is not in a root path")
58+
invalid.append(data_block)
59+
return invalid
60+
61+
@classmethod
62+
def get_root_paths(cls, context):
63+
"""Retrieve and solve all roots from Anatomy() with context data
64+
Will create a list of pathlib.Path"""
65+
anatomy = Anatomy()
66+
for root_name, root_val in anatomy.roots.items():
67+
resolved_path = get_resolved_name(context.data.get('anatomyData', {}), str(root_val))
68+
cls.root_paths.append(Path(resolved_path).resolve())
69+
70+
def process(self, context):
71+
if not self.is_active(context.data):
72+
self.log.debug("Skipping Validate Data Block Paths Location...")
73+
return
74+
75+
# Generate the root Paths
76+
self.get_root_paths(context)
77+
78+
invalid = self.get_invalid(context)
79+
80+
if invalid:
81+
invalid_msg = f"\n-\n".join(bpy.path.abspath(image.filepath) for image in invalid)
82+
root_path_msg = "\n".join(path.as_posix() for path in self.root_paths)
83+
raise PublishValidationError(
84+
f"DataBlock filepath are not in any roots:\n"
85+
f"{invalid_msg}\n"
86+
f"--------------------------------------\n"
87+
f"Validate roots paths are:\n"
88+
f"{root_path_msg}",
89+
title="Invalid Image Path",
90+
description=self.get_description()
91+
)
92+
93+
@classmethod
94+
def get_description(cls):
95+
return inspect.cleandoc("""
96+
### DataBlock filepaths are invalid
97+
Data Block filepaths must be in any of the root paths.
98+
""")

src/quadpype/hosts/tvpaint/api/communication_server.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
import json
34
import time
45
import subprocess
@@ -12,8 +13,11 @@
1213
import threading
1314
import shutil
1415

16+
from pathlib import Path
1517
from contextlib import closing
1618

19+
import semver
20+
1721
from aiohttp import web
1822
from aiohttp_json_rpc import JsonRpc
1923
from aiohttp_json_rpc.protocol import (
@@ -540,6 +544,18 @@ def _windows_file_process(self, src_dst_mapping, to_remove):
540544
# Remove temp folder
541545
shutil.rmtree(tmp_dir)
542546

547+
@staticmethod
548+
def _get_host_version(executable_filename):
549+
version_dict = {'major': 0, 'minor': 0, 'patch': 0}
550+
regex_match = re.search(r"([\d.]+)", executable_filename)
551+
version_elem_list = regex_match.group(1).split(".")
552+
for index, version_elem_key in enumerate(version_dict):
553+
if index >= len(version_elem_list):
554+
break
555+
version_dict[version_elem_key] = int(version_elem_list[index])
556+
557+
return version_dict
558+
543559
def _prepare_windows_plugin(self, launch_args):
544560
"""Copy plugin to TVPaint plugins and set PATH to dependencies.
545561
@@ -549,17 +565,14 @@ def _prepare_windows_plugin(self, launch_args):
549565
to PATH variable.
550566
"""
551567

552-
host_executable = launch_args[0]
553-
executable_file = os.path.basename(host_executable)
568+
host_executable = Path(launch_args[0])
569+
executable_file = host_executable.name
570+
571+
subfolder = "windows_x64"
554572
if "64bit" in executable_file:
555573
subfolder = "windows_x64"
556574
elif "32bit" in executable_file:
557575
subfolder = "windows_x86"
558-
else:
559-
raise ValueError(
560-
"Can't determine if executable "
561-
"leads to 32-bit or 64-bit TVPaint!"
562-
)
563576

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

582595
# Path to TVPaint's plugins folder (where we want to add our plugin)
583-
host_plugins_path = os.path.join(
584-
os.path.dirname(host_executable),
585-
"plugins"
586-
)
596+
host_exe_dir = host_executable.parent
597+
host_exe_version = self._get_host_version(executable_file)
598+
599+
if host_exe_version >= semver.VersionInfo(major=12, minor=0, patch=0):
600+
host_plugins_path = host_exe_dir.joinpath("Resources", "plugins")
601+
else:
602+
host_plugins_path = host_exe_dir.joinpath("plugins")
587603

588604
# Files that must be copied to TVPaint's plugin folder
589605
plugin_dir = os.path.join(source_plugins_dir, "plugin")

src/quadpype/lib/version.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import semver
1010
import requests
11+
from requests.adapters import HTTPAdapter, Retry
1112
from htmllistparse import fetch_listing
1213

1314
from pathlib import Path
@@ -17,6 +18,11 @@
1718

1819
ADDONS_SETTINGS_KEY = "addons"
1920
_NOT_SET = object()
21+
HTTP_ADAPTER = HTTPAdapter(max_retries=Retry(
22+
total=3,
23+
backoff_factor=0.2,
24+
status_forcelist=[500, 502, 503, 504]
25+
))
2026

2127
# Versions should match any string complying with https://semver.org/
2228
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
@@ -201,6 +207,9 @@ def _extract_member(self, member, target_path, pwd):
201207
class PackageHandler:
202208
"""Class for handling a package."""
203209
type = "package"
210+
_request_session = requests.Session()
211+
_request_session.mount('http://', HTTP_ADAPTER)
212+
_request_session.mount('https://', HTTP_ADAPTER)
204213

205214
def __init__(self,
206215
pkg_name: str,
@@ -378,7 +387,7 @@ def get_accessible_remote_source(self):
378387
return remote_source
379388
elif isinstance(remote_source, SourceURL):
380389
try:
381-
response = requests.head(remote_source)
390+
response = self._request_session.get(remote_source)
382391
if response.ok:
383392
return remote_source
384393
except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError):
@@ -637,7 +646,11 @@ def get_versions_from_url(cls, pkg_name: str, source_url: SourceURL, priority_to
637646
if not source_url:
638647
return versions
639648

640-
response = requests.head(source_url)
649+
try:
650+
response = cls._request_session.head(source_url)
651+
except Exception: # noqa
652+
return versions
653+
641654
page_content_type = "text/html"
642655
allowed_content_type = [
643656
"application/zip",
@@ -673,7 +686,11 @@ def get_versions_from_url(cls, pkg_name: str, source_url: SourceURL, priority_to
673686

674687
continue
675688

676-
response = requests.head(item_full_url)
689+
try:
690+
response = cls._request_session.head(item_full_url)
691+
except Exception: # noqa
692+
continue
693+
677694
if response.status_code != 200 or response.headers.get("content-type") not in allowed_content_type:
678695
continue
679696

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

808825
@staticmethod
809826
def _download_version(remote_version: PackageVersion, dest_archive_path: Path):
810-
response = requests.get(remote_version.location, stream=True)
827+
try:
828+
response = PackageHandler._request_session.get(remote_version.location, stream=True)
829+
except Exception as e: # noqa
830+
raise Exception(f"Failed to download {remote_version.location}. Error: {e}")
811831

812832
if response.status_code == 200:
813833
with open(dest_archive_path, "wb") as file:

src/quadpype/pipeline/anatomy.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def root_environmets_fill_data(self, template=None):
134134
template (str): Template for environment variable key fill.
135135
By default is set to `"${}"`.
136136
"""
137-
return self.roots_obj.root_environmets_fill_data(template)
137+
return self.roots_obj.root_environments_fill_data(template)
138138

139139
def find_root_template_from_path(self, *args, **kwargs):
140140
"""Wrapper for Roots `find_root_template_from_path`."""
@@ -1400,7 +1400,7 @@ def _root_environments(self, keys=None, roots=None):
14001400
output.update(self._root_environments(_keys, _value))
14011401
return output
14021402

1403-
def root_environmets_fill_data(self, template=None):
1403+
def root_environments_fill_data(self, template=None):
14041404
"""Environment variable values in dictionary for rootless path.
14051405
14061406
Args:
@@ -1409,12 +1409,12 @@ def root_environmets_fill_data(self, template=None):
14091409
"""
14101410
if template is None:
14111411
template = "${}"
1412-
return self._root_environmets_fill_data(template)
1412+
return self._root_environments_fill_data(template)
14131413

1414-
def _root_environmets_fill_data(self, template, keys=None, roots=None):
1414+
def _root_environments_fill_data(self, template, keys=None, roots=None):
14151415
if keys is None and roots is None:
14161416
return {
1417-
"root": self._root_environmets_fill_data(
1417+
"root": self._root_environments_fill_data(
14181418
template, [], self.roots
14191419
)
14201420
}
@@ -1430,7 +1430,7 @@ def _root_environmets_fill_data(self, template, keys=None, roots=None):
14301430
for key, value in roots.items():
14311431
_keys = list(keys)
14321432
_keys.append(key)
1433-
output[key] = self._root_environmets_fill_data(
1433+
output[key] = self._root_environments_fill_data(
14341434
template, _keys, value
14351435
)
14361436
return output

0 commit comments

Comments
 (0)