mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Merge branch 'develop' into tests/publish_process
This commit is contained in:
commit
8351fee8f5
518 changed files with 33236 additions and 8051 deletions
26
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
26
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,19 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.16.4
|
||||
- 3.16.4-nightly.3
|
||||
- 3.16.4-nightly.2
|
||||
- 3.16.4-nightly.1
|
||||
- 3.16.3
|
||||
- 3.16.3-nightly.5
|
||||
- 3.16.3-nightly.4
|
||||
- 3.16.3-nightly.3
|
||||
- 3.16.3-nightly.2
|
||||
- 3.16.3-nightly.1
|
||||
- 3.16.2
|
||||
- 3.16.2-nightly.2
|
||||
- 3.16.2-nightly.1
|
||||
- 3.16.1
|
||||
- 3.16.0
|
||||
- 3.16.0-nightly.2
|
||||
|
|
@ -122,19 +135,6 @@ body:
|
|||
- 3.14.8-nightly.2
|
||||
- 3.14.8-nightly.1
|
||||
- 3.14.7
|
||||
- 3.14.7-nightly.8
|
||||
- 3.14.7-nightly.7
|
||||
- 3.14.7-nightly.6
|
||||
- 3.14.7-nightly.5
|
||||
- 3.14.7-nightly.4
|
||||
- 3.14.7-nightly.3
|
||||
- 3.14.7-nightly.2
|
||||
- 3.14.7-nightly.1
|
||||
- 3.14.6
|
||||
- 3.14.6-nightly.3
|
||||
- 3.14.6-nightly.2
|
||||
- 3.14.6-nightly.1
|
||||
- 3.14.5
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -37,7 +37,7 @@ Temporary Items
|
|||
###########
|
||||
/build
|
||||
/dist/
|
||||
/server_addon/package/*
|
||||
/server_addon/packages/*
|
||||
|
||||
/vendor/bin/*
|
||||
/vendor/python/*
|
||||
|
|
|
|||
1340
CHANGELOG.md
1340
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -32,12 +32,16 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n
|
|||
wget \
|
||||
gcc \
|
||||
zlib-devel \
|
||||
pcre-devel \
|
||||
perl-core \
|
||||
bzip2 \
|
||||
bzip2-devel \
|
||||
readline-devel \
|
||||
sqlite sqlite-devel \
|
||||
openssl-devel \
|
||||
openssl-libs \
|
||||
openssl11-devel \
|
||||
openssl11-libs \
|
||||
tk-devel libffi-devel \
|
||||
patchelf \
|
||||
automake \
|
||||
|
|
@ -71,7 +75,12 @@ RUN echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/.bashrc \
|
|||
&& echo 'eval "$(pyenv init -)"' >> $HOME/.bashrc \
|
||||
&& echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/.bashrc \
|
||||
&& echo 'eval "$(pyenv init --path)"' >> $HOME/.bashrc
|
||||
RUN source $HOME/.bashrc && pyenv install ${OPENPYPE_PYTHON_VERSION}
|
||||
RUN source $HOME/.bashrc \
|
||||
&& export CPPFLAGS="-I/usr/include/openssl11" \
|
||||
&& export LDFLAGS="-L/usr/lib64/openssl11 -lssl -lcrypto" \
|
||||
&& export PATH=/usr/local/openssl/bin:$PATH \
|
||||
&& export LD_LIBRARY_PATH=/usr/local/openssl/lib:$LD_LIBRARY_PATH \
|
||||
&& pyenv install ${OPENPYPE_PYTHON_VERSION}
|
||||
|
||||
COPY . /opt/openpype/
|
||||
RUN rm -rf /openpype/.poetry || echo "No Poetry installed yet."
|
||||
|
|
@ -93,12 +102,13 @@ RUN source $HOME/.bashrc \
|
|||
RUN source $HOME/.bashrc \
|
||||
&& ./tools/fetch_thirdparty_libs.sh
|
||||
|
||||
RUN echo 'export PYTHONPATH="/opt/openpype/vendor/python:$PYTHONPATH"'>> $HOME/.bashrc
|
||||
RUN source $HOME/.bashrc \
|
||||
&& bash ./tools/build.sh
|
||||
|
||||
RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /usr/lib64/openssl11/libssl* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /usr/lib64/openssl11/libcrypto* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.9/vendor/python/PySide2/Qt/lib
|
||||
|
||||
|
|
|
|||
483
ayon_start.py
483
ayon_start.py
|
|
@ -1,483 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Main entry point for AYON command.
|
||||
|
||||
Bootstrapping process of AYON.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import site
|
||||
import traceback
|
||||
import contextlib
|
||||
|
||||
|
||||
# Enabled logging debug mode when "--debug" is passed
|
||||
if "--verbose" in sys.argv:
|
||||
expected_values = (
|
||||
"Expected: notset, debug, info, warning, error, critical"
|
||||
" or integer [0-50]."
|
||||
)
|
||||
idx = sys.argv.index("--verbose")
|
||||
sys.argv.pop(idx)
|
||||
if idx < len(sys.argv):
|
||||
value = sys.argv.pop(idx)
|
||||
else:
|
||||
raise RuntimeError((
|
||||
f"Expect value after \"--verbose\" argument. {expected_values}"
|
||||
))
|
||||
|
||||
log_level = None
|
||||
low_value = value.lower()
|
||||
if low_value.isdigit():
|
||||
log_level = int(low_value)
|
||||
elif low_value == "notset":
|
||||
log_level = 0
|
||||
elif low_value == "debug":
|
||||
log_level = 10
|
||||
elif low_value == "info":
|
||||
log_level = 20
|
||||
elif low_value == "warning":
|
||||
log_level = 30
|
||||
elif low_value == "error":
|
||||
log_level = 40
|
||||
elif low_value == "critical":
|
||||
log_level = 50
|
||||
|
||||
if log_level is None:
|
||||
raise ValueError((
|
||||
"Unexpected value after \"--verbose\" "
|
||||
f"argument \"{value}\". {expected_values}"
|
||||
))
|
||||
|
||||
os.environ["OPENPYPE_LOG_LEVEL"] = str(log_level)
|
||||
os.environ["AYON_LOG_LEVEL"] = str(log_level)
|
||||
|
||||
# Enable debug mode, may affect log level if log level is not defined
|
||||
if "--debug" in sys.argv:
|
||||
sys.argv.remove("--debug")
|
||||
os.environ["AYON_DEBUG"] = "1"
|
||||
os.environ["OPENPYPE_DEBUG"] = "1"
|
||||
|
||||
if "--automatic-tests" in sys.argv:
|
||||
sys.argv.remove("--automatic-tests")
|
||||
os.environ["IS_TEST"] = "1"
|
||||
|
||||
SKIP_HEADERS = False
|
||||
if "--skip-headers" in sys.argv:
|
||||
sys.argv.remove("--skip-headers")
|
||||
SKIP_HEADERS = True
|
||||
|
||||
SKIP_BOOTSTRAP = False
|
||||
if "--skip-bootstrap" in sys.argv:
|
||||
sys.argv.remove("--skip-bootstrap")
|
||||
SKIP_BOOTSTRAP = True
|
||||
|
||||
if "--use-staging" in sys.argv:
|
||||
sys.argv.remove("--use-staging")
|
||||
os.environ["AYON_USE_STAGING"] = "1"
|
||||
os.environ["OPENPYPE_USE_STAGING"] = "1"
|
||||
|
||||
if "--headless" in sys.argv:
|
||||
os.environ["AYON_HEADLESS_MODE"] = "1"
|
||||
os.environ["OPENPYPE_HEADLESS_MODE"] = "1"
|
||||
sys.argv.remove("--headless")
|
||||
|
||||
elif (
|
||||
os.getenv("AYON_HEADLESS_MODE") != "1"
|
||||
or os.getenv("OPENPYPE_HEADLESS_MODE") != "1"
|
||||
):
|
||||
os.environ.pop("AYON_HEADLESS_MODE", None)
|
||||
os.environ.pop("OPENPYPE_HEADLESS_MODE", None)
|
||||
|
||||
elif (
|
||||
os.getenv("AYON_HEADLESS_MODE")
|
||||
!= os.getenv("OPENPYPE_HEADLESS_MODE")
|
||||
):
|
||||
os.environ["OPENPYPE_HEADLESS_MODE"] = (
|
||||
os.environ["AYON_HEADLESS_MODE"]
|
||||
)
|
||||
|
||||
IS_BUILT_APPLICATION = getattr(sys, "frozen", False)
|
||||
HEADLESS_MODE_ENABLED = os.getenv("AYON_HEADLESS_MODE") == "1"
|
||||
|
||||
_pythonpath = os.getenv("PYTHONPATH", "")
|
||||
_python_paths = _pythonpath.split(os.pathsep)
|
||||
if not IS_BUILT_APPLICATION:
|
||||
# Code root defined by `start.py` directory
|
||||
AYON_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
_dependencies_path = site.getsitepackages()[-1]
|
||||
else:
|
||||
AYON_ROOT = os.path.dirname(sys.executable)
|
||||
|
||||
# add dependencies folder to sys.pat for frozen code
|
||||
_dependencies_path = os.path.normpath(
|
||||
os.path.join(AYON_ROOT, "dependencies")
|
||||
)
|
||||
# add stuff from `<frozen>/dependencies` to PYTHONPATH.
|
||||
sys.path.append(_dependencies_path)
|
||||
_python_paths.append(_dependencies_path)
|
||||
|
||||
# Vendored python modules that must not be in PYTHONPATH environment but
|
||||
# are required for OpenPype processes
|
||||
sys.path.insert(0, os.path.join(AYON_ROOT, "vendor", "python"))
|
||||
|
||||
# Add common package to sys path
|
||||
# - common contains common code for bootstraping and OpenPype processes
|
||||
sys.path.insert(0, os.path.join(AYON_ROOT, "common"))
|
||||
|
||||
# This is content of 'core' addon which is ATM part of build
|
||||
common_python_vendor = os.path.join(
|
||||
AYON_ROOT,
|
||||
"openpype",
|
||||
"vendor",
|
||||
"python",
|
||||
"common"
|
||||
)
|
||||
# Add tools dir to sys path for pyblish UI discovery
|
||||
tools_dir = os.path.join(AYON_ROOT, "openpype", "tools")
|
||||
for path in (AYON_ROOT, common_python_vendor, tools_dir):
|
||||
while path in _python_paths:
|
||||
_python_paths.remove(path)
|
||||
|
||||
while path in sys.path:
|
||||
sys.path.remove(path)
|
||||
|
||||
_python_paths.insert(0, path)
|
||||
sys.path.insert(0, path)
|
||||
|
||||
os.environ["PYTHONPATH"] = os.pathsep.join(_python_paths)
|
||||
|
||||
# enabled AYON state
|
||||
os.environ["USE_AYON_SERVER"] = "1"
|
||||
# Set this to point either to `python` from venv in case of live code
|
||||
# or to `ayon` or `ayon_console` in case of frozen code
|
||||
os.environ["AYON_EXECUTABLE"] = sys.executable
|
||||
os.environ["OPENPYPE_EXECUTABLE"] = sys.executable
|
||||
os.environ["AYON_ROOT"] = AYON_ROOT
|
||||
os.environ["OPENPYPE_ROOT"] = AYON_ROOT
|
||||
os.environ["OPENPYPE_REPOS_ROOT"] = AYON_ROOT
|
||||
os.environ["AYON_MENU_LABEL"] = "AYON"
|
||||
os.environ["AVALON_LABEL"] = "AYON"
|
||||
# Set name of pyblish UI import
|
||||
os.environ["PYBLISH_GUI"] = "pyblish_pype"
|
||||
# Set builtin OCIO root
|
||||
os.environ["BUILTIN_OCIO_ROOT"] = os.path.join(
|
||||
AYON_ROOT,
|
||||
"vendor",
|
||||
"bin",
|
||||
"ocioconfig",
|
||||
"OpenColorIOConfigs"
|
||||
)
|
||||
|
||||
import blessed # noqa: E402
|
||||
import certifi # noqa: E402
|
||||
|
||||
|
||||
if sys.__stdout__:
|
||||
term = blessed.Terminal()
|
||||
|
||||
def _print(message: str):
|
||||
if message.startswith("!!! "):
|
||||
print(f'{term.orangered2("!!! ")}{message[4:]}')
|
||||
elif message.startswith(">>> "):
|
||||
print(f'{term.aquamarine3(">>> ")}{message[4:]}')
|
||||
elif message.startswith("--- "):
|
||||
print(f'{term.darkolivegreen3("--- ")}{message[4:]}')
|
||||
elif message.startswith("*** "):
|
||||
print(f'{term.gold("*** ")}{message[4:]}')
|
||||
elif message.startswith(" - "):
|
||||
print(f'{term.wheat(" - ")}{message[4:]}')
|
||||
elif message.startswith(" . "):
|
||||
print(f'{term.tan(" . ")}{message[4:]}')
|
||||
elif message.startswith(" - "):
|
||||
print(f'{term.seagreen3(" - ")}{message[7:]}')
|
||||
elif message.startswith(" ! "):
|
||||
print(f'{term.goldenrod(" ! ")}{message[7:]}')
|
||||
elif message.startswith(" * "):
|
||||
print(f'{term.aquamarine1(" * ")}{message[7:]}')
|
||||
elif message.startswith(" "):
|
||||
print(f'{term.darkseagreen3(" ")}{message[4:]}')
|
||||
else:
|
||||
print(message)
|
||||
else:
|
||||
def _print(message: str):
|
||||
print(message)
|
||||
|
||||
|
||||
# if SSL_CERT_FILE is not set prior to OpenPype launch, we set it to point
|
||||
# to certifi bundle to make sure we have reasonably new CA certificates.
|
||||
if not os.getenv("SSL_CERT_FILE"):
|
||||
os.environ["SSL_CERT_FILE"] = certifi.where()
|
||||
elif os.getenv("SSL_CERT_FILE") != certifi.where():
|
||||
_print("--- your system is set to use custom CA certificate bundle.")
|
||||
|
||||
from ayon_api import get_base_url
|
||||
from ayon_api.constants import SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY
|
||||
from ayon_common import is_staging_enabled
|
||||
from ayon_common.connection.credentials import (
|
||||
ask_to_login_ui,
|
||||
add_server,
|
||||
need_server_or_login,
|
||||
load_environments,
|
||||
set_environments,
|
||||
create_global_connection,
|
||||
confirm_server_login,
|
||||
)
|
||||
from ayon_common.distribution import (
|
||||
AyonDistribution,
|
||||
BundleNotFoundError,
|
||||
show_missing_bundle_information,
|
||||
)
|
||||
|
||||
|
||||
def set_global_environments() -> None:
|
||||
"""Set global OpenPype's environments."""
|
||||
import acre
|
||||
|
||||
from openpype.settings import get_general_environments
|
||||
|
||||
general_env = get_general_environments()
|
||||
|
||||
# first resolve general environment because merge doesn't expect
|
||||
# values to be list.
|
||||
# TODO: switch to OpenPype environment functions
|
||||
merged_env = acre.merge(
|
||||
acre.compute(acre.parse(general_env), cleanup=False),
|
||||
dict(os.environ)
|
||||
)
|
||||
env = acre.compute(
|
||||
merged_env,
|
||||
cleanup=False
|
||||
)
|
||||
os.environ.clear()
|
||||
os.environ.update(env)
|
||||
|
||||
# Hardcoded default values
|
||||
os.environ["PYBLISH_GUI"] = "pyblish_pype"
|
||||
# Change scale factor only if is not set
|
||||
if "QT_AUTO_SCREEN_SCALE_FACTOR" not in os.environ:
|
||||
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
|
||||
|
||||
|
||||
def set_addons_environments():
|
||||
"""Set global environments for OpenPype modules.
|
||||
|
||||
This requires to have OpenPype in `sys.path`.
|
||||
"""
|
||||
|
||||
import acre
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
modules_manager = ModulesManager()
|
||||
|
||||
# Merge environments with current environments and update values
|
||||
if module_envs := modules_manager.collect_global_environments():
|
||||
parsed_envs = acre.parse(module_envs)
|
||||
env = acre.merge(parsed_envs, dict(os.environ))
|
||||
os.environ.clear()
|
||||
os.environ.update(env)
|
||||
|
||||
|
||||
def _connect_to_ayon_server():
|
||||
load_environments()
|
||||
if not need_server_or_login():
|
||||
create_global_connection()
|
||||
return
|
||||
|
||||
if HEADLESS_MODE_ENABLED:
|
||||
_print("!!! Cannot open v4 Login dialog in headless mode.")
|
||||
_print((
|
||||
"!!! Please use `{}` to specify server address"
|
||||
" and '{}' to specify user's token."
|
||||
).format(SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY))
|
||||
sys.exit(1)
|
||||
|
||||
current_url = os.environ.get(SERVER_URL_ENV_KEY)
|
||||
url, token, username = ask_to_login_ui(current_url, always_on_top=True)
|
||||
if url is not None and token is not None:
|
||||
confirm_server_login(url, token, username)
|
||||
return
|
||||
|
||||
if url is not None:
|
||||
add_server(url, username)
|
||||
|
||||
_print("!!! Login was not successful.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def _check_and_update_from_ayon_server():
|
||||
"""Gets addon info from v4, compares with local folder and updates it.
|
||||
|
||||
Raises:
|
||||
RuntimeError
|
||||
"""
|
||||
|
||||
distribution = AyonDistribution()
|
||||
bundle = None
|
||||
bundle_name = None
|
||||
try:
|
||||
bundle = distribution.bundle_to_use
|
||||
if bundle is not None:
|
||||
bundle_name = bundle.name
|
||||
except BundleNotFoundError as exc:
|
||||
bundle_name = exc.bundle_name
|
||||
|
||||
if bundle is None:
|
||||
url = get_base_url()
|
||||
if not HEADLESS_MODE_ENABLED:
|
||||
show_missing_bundle_information(url, bundle_name)
|
||||
|
||||
elif bundle_name:
|
||||
_print((
|
||||
f"!!! Requested release bundle '{bundle_name}'"
|
||||
" is not available on server."
|
||||
))
|
||||
_print(
|
||||
"!!! Check if selected release bundle"
|
||||
f" is available on the server '{url}'."
|
||||
)
|
||||
|
||||
else:
|
||||
mode = "staging" if is_staging_enabled() else "production"
|
||||
_print(
|
||||
f"!!! No release bundle is set as {mode} on the AYON server."
|
||||
)
|
||||
_print(
|
||||
"!!! Make sure there is a release bundle set"
|
||||
f" as \"{mode}\" on the AYON server '{url}'."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
distribution.distribute()
|
||||
distribution.validate_distribution()
|
||||
os.environ["AYON_BUNDLE_NAME"] = bundle_name
|
||||
|
||||
python_paths = [
|
||||
path
|
||||
for path in os.getenv("PYTHONPATH", "").split(os.pathsep)
|
||||
if path
|
||||
]
|
||||
|
||||
for path in distribution.get_sys_paths():
|
||||
sys.path.insert(0, path)
|
||||
if path not in python_paths:
|
||||
python_paths.append(path)
|
||||
os.environ["PYTHONPATH"] = os.pathsep.join(python_paths)
|
||||
|
||||
|
||||
def boot():
|
||||
"""Bootstrap OpenPype."""
|
||||
|
||||
from openpype.version import __version__
|
||||
|
||||
# TODO load version
|
||||
os.environ["OPENPYPE_VERSION"] = __version__
|
||||
os.environ["AYON_VERSION"] = __version__
|
||||
|
||||
_connect_to_ayon_server()
|
||||
_check_and_update_from_ayon_server()
|
||||
|
||||
# delete OpenPype module and it's submodules from cache so it is used from
|
||||
# specific version
|
||||
modules_to_del = [
|
||||
sys.modules.pop(module_name)
|
||||
for module_name in tuple(sys.modules)
|
||||
if module_name == "openpype" or module_name.startswith("openpype.")
|
||||
]
|
||||
|
||||
for module_name in modules_to_del:
|
||||
with contextlib.suppress(AttributeError, KeyError):
|
||||
del sys.modules[module_name]
|
||||
|
||||
|
||||
def main_cli():
|
||||
from openpype import cli
|
||||
from openpype.version import __version__
|
||||
from openpype.lib import terminal as t
|
||||
|
||||
_print(">>> loading environments ...")
|
||||
_print(" - global AYON ...")
|
||||
set_global_environments()
|
||||
_print(" - for addons ...")
|
||||
set_addons_environments()
|
||||
|
||||
# print info when not running scripts defined in 'silent commands'
|
||||
if not SKIP_HEADERS:
|
||||
info = get_info(is_staging_enabled())
|
||||
info.insert(0, f">>> Using AYON from [ {AYON_ROOT} ]")
|
||||
|
||||
t_width = 20
|
||||
with contextlib.suppress(ValueError, OSError):
|
||||
t_width = os.get_terminal_size().columns - 2
|
||||
|
||||
_header = f"*** AYON [{__version__}] "
|
||||
info.insert(0, _header + "-" * (t_width - len(_header)))
|
||||
|
||||
for i in info:
|
||||
t.echo(i)
|
||||
|
||||
try:
|
||||
cli.main(obj={}, prog_name="ayon")
|
||||
except Exception: # noqa
|
||||
exc_info = sys.exc_info()
|
||||
_print("!!! AYON crashed:")
|
||||
traceback.print_exception(*exc_info)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def script_cli():
|
||||
"""Run and execute script."""
|
||||
|
||||
filepath = os.path.abspath(sys.argv[1])
|
||||
|
||||
# Find '__main__.py' in directory
|
||||
if os.path.isdir(filepath):
|
||||
new_filepath = os.path.join(filepath, "__main__.py")
|
||||
if not os.path.exists(new_filepath):
|
||||
raise RuntimeError(
|
||||
f"can't find '__main__' module in '{filepath}'")
|
||||
filepath = new_filepath
|
||||
|
||||
# Add parent dir to sys path
|
||||
sys.path.insert(0, os.path.dirname(filepath))
|
||||
|
||||
# Read content and execute
|
||||
with open(filepath, "r") as stream:
|
||||
content = stream.read()
|
||||
|
||||
exec(compile(content, filepath, "exec"), globals())
|
||||
|
||||
|
||||
def get_info(use_staging=None) -> list:
|
||||
"""Print additional information to console."""
|
||||
|
||||
inf = []
|
||||
if use_staging:
|
||||
inf.append(("AYON variant", "staging"))
|
||||
else:
|
||||
inf.append(("AYON variant", "production"))
|
||||
inf.append(("AYON bundle", os.getenv("AYON_BUNDLE")))
|
||||
|
||||
# NOTE add addons information
|
||||
|
||||
maximum = max(len(i[0]) for i in inf)
|
||||
formatted = []
|
||||
for info in inf:
|
||||
padding = (maximum - len(info[0])) + 1
|
||||
formatted.append(f'... {info[0]}:{" " * padding}[ {info[1]} ]')
|
||||
return formatted
|
||||
|
||||
|
||||
def main():
|
||||
if not SKIP_BOOTSTRAP:
|
||||
boot()
|
||||
|
||||
args = list(sys.argv)
|
||||
args.pop(0)
|
||||
if args and os.path.exists(args[0]):
|
||||
script_cli()
|
||||
else:
|
||||
main_cli()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
from .utils import (
|
||||
IS_BUILT_APPLICATION,
|
||||
is_staging_enabled,
|
||||
get_local_site_id,
|
||||
get_ayon_appdirs,
|
||||
get_ayon_launch_args,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"IS_BUILT_APPLICATION",
|
||||
"is_staging_enabled",
|
||||
"get_local_site_id",
|
||||
"get_ayon_appdirs",
|
||||
"get_ayon_launch_args",
|
||||
)
|
||||
|
|
@ -1,511 +0,0 @@
|
|||
"""Handle credentials and connection to server for client application.
|
||||
|
||||
Cache and store used server urls. Store/load API keys to/from keyring if
|
||||
needed. Store metadata about used urls, usernames for the urls and when was
|
||||
the connection with the username established.
|
||||
|
||||
On bootstrap is created global connection with information about site and
|
||||
client version. The connection object lives in 'ayon_api'.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import platform
|
||||
import datetime
|
||||
import contextlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import Optional, Union, Any
|
||||
|
||||
import ayon_api
|
||||
|
||||
from ayon_api.constants import SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY
|
||||
from ayon_api.exceptions import UrlError
|
||||
from ayon_api.utils import (
|
||||
validate_url,
|
||||
is_token_valid,
|
||||
logout_from_server,
|
||||
)
|
||||
|
||||
from ayon_common.utils import (
|
||||
get_ayon_appdirs,
|
||||
get_local_site_id,
|
||||
get_ayon_launch_args,
|
||||
is_staging_enabled,
|
||||
)
|
||||
|
||||
|
||||
class ChangeUserResult:
|
||||
def __init__(
|
||||
self, logged_out, old_url, old_token, old_username,
|
||||
new_url, new_token, new_username
|
||||
):
|
||||
shutdown = logged_out
|
||||
restart = new_url is not None and new_url != old_url
|
||||
token_changed = new_token is not None and new_token != old_token
|
||||
|
||||
self.logged_out = logged_out
|
||||
self.old_url = old_url
|
||||
self.old_token = old_token
|
||||
self.old_username = old_username
|
||||
self.new_url = new_url
|
||||
self.new_token = new_token
|
||||
self.new_username = new_username
|
||||
|
||||
self.shutdown = shutdown
|
||||
self.restart = restart
|
||||
self.token_changed = token_changed
|
||||
|
||||
|
||||
def _get_servers_path():
|
||||
return get_ayon_appdirs("used_servers.json")
|
||||
|
||||
|
||||
def get_servers_info_data():
|
||||
"""Metadata about used server on this machine.
|
||||
|
||||
Store data about all used server urls, last used url and user username for
|
||||
the url. Using this metadata we can remember which username was used per
|
||||
url if token stored in keyring loose lifetime.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Information about servers.
|
||||
"""
|
||||
|
||||
data = {}
|
||||
servers_info_path = _get_servers_path()
|
||||
if not os.path.exists(servers_info_path):
|
||||
dirpath = os.path.dirname(servers_info_path)
|
||||
if not os.path.exists(dirpath):
|
||||
os.makedirs(dirpath)
|
||||
|
||||
return data
|
||||
|
||||
with open(servers_info_path, "r") as stream:
|
||||
with contextlib.suppress(BaseException):
|
||||
data = json.load(stream)
|
||||
return data
|
||||
|
||||
|
||||
def add_server(url: str, username: str):
|
||||
"""Add server to server info metadata.
|
||||
|
||||
This function will also mark the url as last used url on the machine so on
|
||||
next launch will be used.
|
||||
|
||||
Args:
|
||||
url (str): Server url.
|
||||
username (str): Name of user used to log in.
|
||||
"""
|
||||
|
||||
servers_info_path = _get_servers_path()
|
||||
data = get_servers_info_data()
|
||||
data["last_server"] = url
|
||||
if "urls" not in data:
|
||||
data["urls"] = {}
|
||||
data["urls"][url] = {
|
||||
"updated_dt": datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
|
||||
"username": username,
|
||||
}
|
||||
|
||||
with open(servers_info_path, "w") as stream:
|
||||
json.dump(data, stream)
|
||||
|
||||
|
||||
def remove_server(url: str):
|
||||
"""Remove server url from servers information.
|
||||
|
||||
This should be used on logout to completelly loose information about server
|
||||
on the machine.
|
||||
|
||||
Args:
|
||||
url (str): Server url.
|
||||
"""
|
||||
|
||||
if not url:
|
||||
return
|
||||
|
||||
servers_info_path = _get_servers_path()
|
||||
data = get_servers_info_data()
|
||||
if data.get("last_server") == url:
|
||||
data["last_server"] = None
|
||||
|
||||
if "urls" in data:
|
||||
data["urls"].pop(url, None)
|
||||
|
||||
with open(servers_info_path, "w") as stream:
|
||||
json.dump(data, stream)
|
||||
|
||||
|
||||
def get_last_server(
|
||||
data: Optional[dict[str, Any]] = None
|
||||
) -> Union[str, None]:
|
||||
"""Last server used to log in on this machine.
|
||||
|
||||
Args:
|
||||
data (Optional[dict[str, Any]]): Prepared server information data.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Last used server url.
|
||||
"""
|
||||
|
||||
if data is None:
|
||||
data = get_servers_info_data()
|
||||
return data.get("last_server")
|
||||
|
||||
|
||||
def get_last_username_by_url(
|
||||
url: str,
|
||||
data: Optional[dict[str, Any]] = None
|
||||
) -> Union[str, None]:
|
||||
"""Get last username which was used for passed url.
|
||||
|
||||
Args:
|
||||
url (str): Server url.
|
||||
data (Optional[dict[str, Any]]): Servers info.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Username.
|
||||
"""
|
||||
|
||||
if not url:
|
||||
return None
|
||||
|
||||
if data is None:
|
||||
data = get_servers_info_data()
|
||||
|
||||
if urls := data.get("urls"):
|
||||
if url_info := urls.get(url):
|
||||
return url_info.get("username")
|
||||
return None
|
||||
|
||||
|
||||
def get_last_server_with_username():
|
||||
"""Receive last server and username used in last connection.
|
||||
|
||||
Returns:
|
||||
tuple[Union[str, None], Union[str, None]]: Url and username.
|
||||
"""
|
||||
|
||||
data = get_servers_info_data()
|
||||
url = get_last_server(data)
|
||||
username = get_last_username_by_url(url)
|
||||
return url, username
|
||||
|
||||
|
||||
class TokenKeyring:
|
||||
# Fake username with hardcoded username
|
||||
username_key = "username"
|
||||
|
||||
def __init__(self, url):
|
||||
try:
|
||||
import keyring
|
||||
|
||||
except Exception as exc:
|
||||
raise NotImplementedError(
|
||||
"Python module `keyring` is not available."
|
||||
) from exc
|
||||
|
||||
# hack for cx_freeze and Windows keyring backend
|
||||
if platform.system().lower() == "windows":
|
||||
from keyring.backends import Windows
|
||||
|
||||
keyring.set_keyring(Windows.WinVaultKeyring())
|
||||
|
||||
self._url = url
|
||||
self._keyring_key = f"AYON/{url}"
|
||||
|
||||
def get_value(self):
|
||||
import keyring
|
||||
|
||||
return keyring.get_password(self._keyring_key, self.username_key)
|
||||
|
||||
def set_value(self, value):
|
||||
import keyring
|
||||
|
||||
if value is not None:
|
||||
keyring.set_password(self._keyring_key, self.username_key, value)
|
||||
return
|
||||
|
||||
with contextlib.suppress(keyring.errors.PasswordDeleteError):
|
||||
keyring.delete_password(self._keyring_key, self.username_key)
|
||||
|
||||
|
||||
def load_token(url: str) -> Union[str, None]:
|
||||
"""Get token for url from keyring.
|
||||
|
||||
Args:
|
||||
url (str): Server url.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Token for passed url available in keyring.
|
||||
"""
|
||||
|
||||
return TokenKeyring(url).get_value()
|
||||
|
||||
|
||||
def store_token(url: str, token: str):
|
||||
"""Store token by url to keyring.
|
||||
|
||||
Args:
|
||||
url (str): Server url.
|
||||
token (str): User token to server.
|
||||
"""
|
||||
|
||||
TokenKeyring(url).set_value(token)
|
||||
|
||||
|
||||
def ask_to_login_ui(
|
||||
url: Optional[str] = None,
|
||||
always_on_top: Optional[bool] = False
|
||||
) -> tuple[str, str, str]:
|
||||
"""Ask user to login using UI.
|
||||
|
||||
This should be used only when user is not yet logged in at all or available
|
||||
credentials are invalid. To change credentials use 'change_user_ui'
|
||||
function.
|
||||
|
||||
Use a subprocess to show UI.
|
||||
|
||||
Args:
|
||||
url (Optional[str]): Server url that could be prefilled in UI.
|
||||
always_on_top (Optional[bool]): Window will be drawn on top of
|
||||
other windows.
|
||||
|
||||
Returns:
|
||||
tuple[str, str, str]: Url, user's token and username.
|
||||
"""
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
ui_dir = os.path.join(current_dir, "ui")
|
||||
|
||||
if url is None:
|
||||
url = get_last_server()
|
||||
username = get_last_username_by_url(url)
|
||||
data = {
|
||||
"url": url,
|
||||
"username": username,
|
||||
"always_on_top": always_on_top,
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", prefix="ayon_login", suffix=".json", delete=False
|
||||
) as tmp:
|
||||
output = tmp.name
|
||||
json.dump(data, tmp)
|
||||
|
||||
code = subprocess.call(
|
||||
get_ayon_launch_args(ui_dir, "--skip-bootstrap", output))
|
||||
if code != 0:
|
||||
raise RuntimeError("Failed to show login UI")
|
||||
|
||||
with open(output, "r") as stream:
|
||||
data = json.load(stream)
|
||||
os.remove(output)
|
||||
return data["output"]
|
||||
|
||||
|
||||
def change_user_ui() -> ChangeUserResult:
|
||||
"""Change user using UI.
|
||||
|
||||
Show UI to user where he can change credentials or url. Output will contain
|
||||
all information about old/new values of url, username, api key. If user
|
||||
confirmed or declined values.
|
||||
|
||||
Returns:
|
||||
ChangeUserResult: Information about user change.
|
||||
"""
|
||||
|
||||
from .ui import change_user
|
||||
|
||||
url, username = get_last_server_with_username()
|
||||
token = load_token(url)
|
||||
result = change_user(url, username, token)
|
||||
new_url, new_token, new_username, logged_out = result
|
||||
|
||||
output = ChangeUserResult(
|
||||
logged_out, url, token, username,
|
||||
new_url, new_token, new_username
|
||||
)
|
||||
if output.logged_out:
|
||||
logout(url, token)
|
||||
|
||||
elif output.token_changed:
|
||||
change_token(
|
||||
output.new_url,
|
||||
output.new_token,
|
||||
output.new_username,
|
||||
output.old_url
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def change_token(
|
||||
url: str,
|
||||
token: str,
|
||||
username: Optional[str] = None,
|
||||
old_url: Optional[str] = None
|
||||
):
|
||||
"""Change url and token in currently running session.
|
||||
|
||||
Function can also change server url, in that case are previous credentials
|
||||
NOT removed from cache.
|
||||
|
||||
Args:
|
||||
url (str): Url to server.
|
||||
token (str): New token to be used for url connection.
|
||||
username (Optional[str]): Username of logged user.
|
||||
old_url (Optional[str]): Previous url. Value from 'get_last_server'
|
||||
is used if not entered.
|
||||
"""
|
||||
|
||||
if old_url is None:
|
||||
old_url = get_last_server()
|
||||
if old_url and old_url == url:
|
||||
remove_url_cache(old_url)
|
||||
|
||||
# TODO check if ayon_api is already connected
|
||||
add_server(url, username)
|
||||
store_token(url, token)
|
||||
ayon_api.change_token(url, token)
|
||||
|
||||
|
||||
def remove_url_cache(url: str):
|
||||
"""Clear cache for server url.
|
||||
|
||||
Args:
|
||||
url (str): Server url which is removed from cache.
|
||||
"""
|
||||
|
||||
store_token(url, None)
|
||||
|
||||
|
||||
def remove_token_cache(url: str, token: str):
|
||||
"""Remove token from local cache of url.
|
||||
|
||||
Is skipped if cached token under the passed url is not the same
|
||||
as passed token.
|
||||
|
||||
Args:
|
||||
url (str): Url to server.
|
||||
token (str): Token to be removed from url cache.
|
||||
"""
|
||||
|
||||
if load_token(url) == token:
|
||||
remove_url_cache(url)
|
||||
|
||||
|
||||
def logout(url: str, token: str):
|
||||
"""Logout from server and throw token away.
|
||||
|
||||
Args:
|
||||
url (str): Url from which should be logged out.
|
||||
token (str): Token which should be used to log out.
|
||||
"""
|
||||
|
||||
remove_server(url)
|
||||
ayon_api.close_connection()
|
||||
ayon_api.set_environments(None, None)
|
||||
remove_token_cache(url, token)
|
||||
logout_from_server(url, token)
|
||||
|
||||
|
||||
def load_environments():
|
||||
"""Load environments on startup.
|
||||
|
||||
Handle environments needed for connection with server. Environments are
|
||||
'AYON_SERVER_URL' and 'AYON_API_KEY'.
|
||||
|
||||
Server is looked up from environment. Already set environent is not
|
||||
changed. If environemnt is not filled then last server stored in appdirs
|
||||
is used.
|
||||
|
||||
Token is skipped if url is not available. Otherwise, is also checked from
|
||||
env and if is not available then uses 'load_token' to try to get token
|
||||
based on server url.
|
||||
"""
|
||||
|
||||
server_url = os.environ.get(SERVER_URL_ENV_KEY)
|
||||
if not server_url:
|
||||
server_url = get_last_server()
|
||||
if not server_url:
|
||||
return
|
||||
os.environ[SERVER_URL_ENV_KEY] = server_url
|
||||
|
||||
if not os.environ.get(SERVER_API_ENV_KEY):
|
||||
if token := load_token(server_url):
|
||||
os.environ[SERVER_API_ENV_KEY] = token
|
||||
|
||||
|
||||
def set_environments(url: str, token: str):
|
||||
"""Change url and token environemnts in currently running process.
|
||||
|
||||
Args:
|
||||
url (str): New server url.
|
||||
token (str): User's token.
|
||||
"""
|
||||
|
||||
ayon_api.set_environments(url, token)
|
||||
|
||||
|
||||
def create_global_connection():
|
||||
"""Create global connection with site id and client version.
|
||||
|
||||
Make sure the global connection in 'ayon_api' have entered site id and
|
||||
client version.
|
||||
|
||||
Set default settings variant to use based on 'is_staging_enabled'.
|
||||
"""
|
||||
|
||||
ayon_api.create_connection(
|
||||
get_local_site_id(), os.environ.get("AYON_VERSION")
|
||||
)
|
||||
ayon_api.set_default_settings_variant(
|
||||
"staging" if is_staging_enabled() else "production"
|
||||
)
|
||||
|
||||
|
||||
def need_server_or_login() -> bool:
|
||||
"""Check if server url or login to the server are needed.
|
||||
|
||||
It is recommended to call 'load_environments' on startup before this check.
|
||||
But in some cases this function could be called after startup.
|
||||
|
||||
Returns:
|
||||
bool: 'True' if server and token are available. Otherwise 'False'.
|
||||
"""
|
||||
|
||||
server_url = os.environ.get(SERVER_URL_ENV_KEY)
|
||||
if not server_url:
|
||||
return True
|
||||
|
||||
try:
|
||||
server_url = validate_url(server_url)
|
||||
except UrlError:
|
||||
return True
|
||||
|
||||
token = os.environ.get(SERVER_API_ENV_KEY)
|
||||
if token:
|
||||
return not is_token_valid(server_url, token)
|
||||
|
||||
token = load_token(server_url)
|
||||
if token:
|
||||
return not is_token_valid(server_url, token)
|
||||
return True
|
||||
|
||||
|
||||
def confirm_server_login(url, token, username):
|
||||
"""Confirm login of user and do necessary stepts to apply changes.
|
||||
|
||||
This should not be used on "change" of user but on first login.
|
||||
|
||||
Args:
|
||||
url (str): Server url where user authenticated.
|
||||
token (str): API token used for authentication to server.
|
||||
username (Union[str, None]): Username related to API token.
|
||||
"""
|
||||
|
||||
add_server(url, username)
|
||||
store_token(url, token)
|
||||
set_environments(url, token)
|
||||
create_global_connection()
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
from .login_window import (
|
||||
ServerLoginWindow,
|
||||
ask_to_login,
|
||||
change_user,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ServerLoginWindow",
|
||||
"ask_to_login",
|
||||
"change_user",
|
||||
)
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import sys
|
||||
import json
|
||||
|
||||
from ayon_common.connection.ui.login_window import ask_to_login
|
||||
|
||||
|
||||
def main(output_path):
|
||||
with open(output_path, "r") as stream:
|
||||
data = json.load(stream)
|
||||
|
||||
url = data.get("url")
|
||||
username = data.get("username")
|
||||
always_on_top = data.get("always_on_top", False)
|
||||
out_url, out_token, out_username = ask_to_login(
|
||||
url, username, always_on_top=always_on_top)
|
||||
|
||||
data["output"] = [out_url, out_token, out_username]
|
||||
with open(output_path, "w") as stream:
|
||||
json.dump(data, stream)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[-1])
|
||||
|
|
@ -1,710 +0,0 @@
|
|||
import traceback
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_api.exceptions import UrlError
|
||||
from ayon_api.utils import validate_url, login_to_server
|
||||
|
||||
from ayon_common.resources import (
|
||||
get_resource_path,
|
||||
get_icon_path,
|
||||
load_stylesheet,
|
||||
)
|
||||
from ayon_common.ui_utils import set_style_property, get_qt_app
|
||||
|
||||
from .widgets import (
|
||||
PressHoverButton,
|
||||
PlaceholderLineEdit,
|
||||
)
|
||||
|
||||
|
||||
class LogoutConfirmDialog(QtWidgets.QDialog):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle("Logout confirmation")
|
||||
|
||||
message_widget = QtWidgets.QWidget(self)
|
||||
|
||||
message_label = QtWidgets.QLabel(
|
||||
(
|
||||
"You are going to logout. This action will close this"
|
||||
" application and will invalidate your login."
|
||||
" All other applications launched with this login won't be"
|
||||
" able to use it anymore.<br/><br/>"
|
||||
"You can cancel logout and only change server and user login"
|
||||
" in login dialog.<br/><br/>"
|
||||
"Press OK to confirm logout."
|
||||
),
|
||||
message_widget
|
||||
)
|
||||
message_label.setWordWrap(True)
|
||||
|
||||
message_layout = QtWidgets.QHBoxLayout(message_widget)
|
||||
message_layout.setContentsMargins(0, 0, 0, 0)
|
||||
message_layout.addWidget(message_label, 1)
|
||||
|
||||
sep_frame = QtWidgets.QFrame(self)
|
||||
sep_frame.setObjectName("Separator")
|
||||
sep_frame.setMinimumHeight(2)
|
||||
sep_frame.setMaximumHeight(2)
|
||||
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", footer_widget)
|
||||
confirm_btn = QtWidgets.QPushButton("OK", footer_widget)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
footer_layout.addStretch(1)
|
||||
footer_layout.addWidget(cancel_btn, 0)
|
||||
footer_layout.addWidget(confirm_btn, 0)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.addWidget(message_widget, 0)
|
||||
main_layout.addStretch(1)
|
||||
main_layout.addWidget(sep_frame, 0)
|
||||
main_layout.addWidget(footer_widget, 0)
|
||||
|
||||
cancel_btn.clicked.connect(self._on_cancel_click)
|
||||
confirm_btn.clicked.connect(self._on_confirm_click)
|
||||
|
||||
self._cancel_btn = cancel_btn
|
||||
self._confirm_btn = confirm_btn
|
||||
self._result = False
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
self._match_btns_sizes()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self._match_btns_sizes()
|
||||
|
||||
def _match_btns_sizes(self):
|
||||
width = max(
|
||||
self._cancel_btn.sizeHint().width(),
|
||||
self._confirm_btn.sizeHint().width()
|
||||
)
|
||||
self._cancel_btn.setMinimumWidth(width)
|
||||
self._confirm_btn.setMinimumWidth(width)
|
||||
|
||||
def _on_cancel_click(self):
|
||||
self._result = False
|
||||
self.reject()
|
||||
|
||||
def _on_confirm_click(self):
|
||||
self._result = True
|
||||
self.accept()
|
||||
|
||||
def get_result(self):
|
||||
return self._result
|
||||
|
||||
|
||||
class ServerLoginWindow(QtWidgets.QDialog):
|
||||
default_width = 410
|
||||
default_height = 170
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
icon_path = get_icon_path()
|
||||
icon = QtGui.QIcon(icon_path)
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowTitle("Login to server")
|
||||
|
||||
edit_icon_path = get_resource_path("edit.png")
|
||||
edit_icon = QtGui.QIcon(edit_icon_path)
|
||||
|
||||
# --- URL page ---
|
||||
login_widget = QtWidgets.QWidget(self)
|
||||
|
||||
user_cred_widget = QtWidgets.QWidget(login_widget)
|
||||
|
||||
url_label = QtWidgets.QLabel("URL:", user_cred_widget)
|
||||
|
||||
url_widget = QtWidgets.QWidget(user_cred_widget)
|
||||
|
||||
url_input = PlaceholderLineEdit(url_widget)
|
||||
url_input.setPlaceholderText("< https://ayon.server.com >")
|
||||
|
||||
url_preview = QtWidgets.QLineEdit(url_widget)
|
||||
url_preview.setReadOnly(True)
|
||||
url_preview.setObjectName("LikeDisabledInput")
|
||||
|
||||
url_edit_btn = PressHoverButton(user_cred_widget)
|
||||
url_edit_btn.setIcon(edit_icon)
|
||||
url_edit_btn.setObjectName("PasswordBtn")
|
||||
|
||||
url_layout = QtWidgets.QHBoxLayout(url_widget)
|
||||
url_layout.setContentsMargins(0, 0, 0, 0)
|
||||
url_layout.addWidget(url_input, 1)
|
||||
url_layout.addWidget(url_preview, 1)
|
||||
|
||||
# --- URL separator ---
|
||||
url_cred_sep = QtWidgets.QFrame(self)
|
||||
url_cred_sep.setObjectName("Separator")
|
||||
url_cred_sep.setMinimumHeight(2)
|
||||
url_cred_sep.setMaximumHeight(2)
|
||||
|
||||
# --- Login page ---
|
||||
username_label = QtWidgets.QLabel("Username:", user_cred_widget)
|
||||
|
||||
username_widget = QtWidgets.QWidget(user_cred_widget)
|
||||
|
||||
username_input = PlaceholderLineEdit(username_widget)
|
||||
username_input.setPlaceholderText("< Artist >")
|
||||
|
||||
username_preview = QtWidgets.QLineEdit(username_widget)
|
||||
username_preview.setReadOnly(True)
|
||||
username_preview.setObjectName("LikeDisabledInput")
|
||||
|
||||
username_edit_btn = PressHoverButton(user_cred_widget)
|
||||
username_edit_btn.setIcon(edit_icon)
|
||||
username_edit_btn.setObjectName("PasswordBtn")
|
||||
|
||||
username_layout = QtWidgets.QHBoxLayout(username_widget)
|
||||
username_layout.setContentsMargins(0, 0, 0, 0)
|
||||
username_layout.addWidget(username_input, 1)
|
||||
username_layout.addWidget(username_preview, 1)
|
||||
|
||||
password_label = QtWidgets.QLabel("Password:", user_cred_widget)
|
||||
password_input = PlaceholderLineEdit(user_cred_widget)
|
||||
password_input.setPlaceholderText("< *********** >")
|
||||
password_input.setEchoMode(PlaceholderLineEdit.Password)
|
||||
|
||||
api_label = QtWidgets.QLabel("API key:", user_cred_widget)
|
||||
api_preview = QtWidgets.QLineEdit(user_cred_widget)
|
||||
api_preview.setReadOnly(True)
|
||||
api_preview.setObjectName("LikeDisabledInput")
|
||||
|
||||
show_password_icon_path = get_resource_path("eye.png")
|
||||
show_password_icon = QtGui.QIcon(show_password_icon_path)
|
||||
show_password_btn = PressHoverButton(user_cred_widget)
|
||||
show_password_btn.setObjectName("PasswordBtn")
|
||||
show_password_btn.setIcon(show_password_icon)
|
||||
show_password_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
|
||||
|
||||
cred_msg_sep = QtWidgets.QFrame(self)
|
||||
cred_msg_sep.setObjectName("Separator")
|
||||
cred_msg_sep.setMinimumHeight(2)
|
||||
cred_msg_sep.setMaximumHeight(2)
|
||||
|
||||
# --- Credentials inputs ---
|
||||
user_cred_layout = QtWidgets.QGridLayout(user_cred_widget)
|
||||
user_cred_layout.setContentsMargins(0, 0, 0, 0)
|
||||
row = 0
|
||||
|
||||
user_cred_layout.addWidget(url_label, row, 0, 1, 1)
|
||||
user_cred_layout.addWidget(url_widget, row, 1, 1, 1)
|
||||
user_cred_layout.addWidget(url_edit_btn, row, 2, 1, 1)
|
||||
row += 1
|
||||
|
||||
user_cred_layout.addWidget(url_cred_sep, row, 0, 1, 3)
|
||||
row += 1
|
||||
|
||||
user_cred_layout.addWidget(username_label, row, 0, 1, 1)
|
||||
user_cred_layout.addWidget(username_widget, row, 1, 1, 1)
|
||||
user_cred_layout.addWidget(username_edit_btn, row, 2, 2, 1)
|
||||
row += 1
|
||||
|
||||
user_cred_layout.addWidget(api_label, row, 0, 1, 1)
|
||||
user_cred_layout.addWidget(api_preview, row, 1, 1, 1)
|
||||
row += 1
|
||||
|
||||
user_cred_layout.addWidget(password_label, row, 0, 1, 1)
|
||||
user_cred_layout.addWidget(password_input, row, 1, 1, 1)
|
||||
user_cred_layout.addWidget(show_password_btn, row, 2, 1, 1)
|
||||
row += 1
|
||||
|
||||
user_cred_layout.addWidget(cred_msg_sep, row, 0, 1, 3)
|
||||
row += 1
|
||||
|
||||
user_cred_layout.setColumnStretch(0, 0)
|
||||
user_cred_layout.setColumnStretch(1, 1)
|
||||
user_cred_layout.setColumnStretch(2, 0)
|
||||
|
||||
login_layout = QtWidgets.QVBoxLayout(login_widget)
|
||||
login_layout.setContentsMargins(0, 0, 0, 0)
|
||||
login_layout.addWidget(user_cred_widget, 1)
|
||||
|
||||
# --- Messages ---
|
||||
# Messages for users (e.g. invalid url etc.)
|
||||
message_label = QtWidgets.QLabel(self)
|
||||
message_label.setWordWrap(True)
|
||||
message_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
logout_btn = QtWidgets.QPushButton("Logout", footer_widget)
|
||||
user_message = QtWidgets.QLabel(footer_widget)
|
||||
login_btn = QtWidgets.QPushButton("Login", footer_widget)
|
||||
confirm_btn = QtWidgets.QPushButton("Confirm", footer_widget)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
footer_layout.addWidget(logout_btn, 0)
|
||||
footer_layout.addWidget(user_message, 1)
|
||||
footer_layout.addWidget(login_btn, 0)
|
||||
footer_layout.addWidget(confirm_btn, 0)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.addWidget(login_widget, 0)
|
||||
main_layout.addWidget(message_label, 0)
|
||||
main_layout.addStretch(1)
|
||||
main_layout.addWidget(footer_widget, 0)
|
||||
|
||||
url_input.textChanged.connect(self._on_url_change)
|
||||
url_input.returnPressed.connect(self._on_url_enter_press)
|
||||
username_input.textChanged.connect(self._on_user_change)
|
||||
username_input.returnPressed.connect(self._on_username_enter_press)
|
||||
password_input.returnPressed.connect(self._on_password_enter_press)
|
||||
show_password_btn.change_state.connect(self._on_show_password)
|
||||
url_edit_btn.clicked.connect(self._on_url_edit_click)
|
||||
username_edit_btn.clicked.connect(self._on_username_edit_click)
|
||||
logout_btn.clicked.connect(self._on_logout_click)
|
||||
login_btn.clicked.connect(self._on_login_click)
|
||||
confirm_btn.clicked.connect(self._on_login_click)
|
||||
|
||||
self._message_label = message_label
|
||||
|
||||
self._url_widget = url_widget
|
||||
self._url_input = url_input
|
||||
self._url_preview = url_preview
|
||||
self._url_edit_btn = url_edit_btn
|
||||
|
||||
self._login_widget = login_widget
|
||||
|
||||
self._user_cred_widget = user_cred_widget
|
||||
self._username_input = username_input
|
||||
self._username_preview = username_preview
|
||||
self._username_edit_btn = username_edit_btn
|
||||
|
||||
self._password_label = password_label
|
||||
self._password_input = password_input
|
||||
self._show_password_btn = show_password_btn
|
||||
self._api_label = api_label
|
||||
self._api_preview = api_preview
|
||||
|
||||
self._logout_btn = logout_btn
|
||||
self._user_message = user_message
|
||||
self._login_btn = login_btn
|
||||
self._confirm_btn = confirm_btn
|
||||
|
||||
self._url_is_valid = None
|
||||
self._credentials_are_valid = None
|
||||
self._result = (None, None, None, False)
|
||||
self._first_show = True
|
||||
|
||||
self._allow_logout = False
|
||||
self._logged_in = False
|
||||
self._url_edit_mode = False
|
||||
self._username_edit_mode = False
|
||||
|
||||
def set_allow_logout(self, allow_logout):
|
||||
if allow_logout is self._allow_logout:
|
||||
return
|
||||
self._allow_logout = allow_logout
|
||||
|
||||
self._update_states_by_edit_mode()
|
||||
|
||||
def _set_logged_in(self, logged_in):
|
||||
if logged_in is self._logged_in:
|
||||
return
|
||||
self._logged_in = logged_in
|
||||
|
||||
self._update_states_by_edit_mode()
|
||||
|
||||
def _set_url_edit_mode(self, edit_mode):
|
||||
if self._url_edit_mode is not edit_mode:
|
||||
self._url_edit_mode = edit_mode
|
||||
self._update_states_by_edit_mode()
|
||||
|
||||
def _set_username_edit_mode(self, edit_mode):
|
||||
if self._username_edit_mode is not edit_mode:
|
||||
self._username_edit_mode = edit_mode
|
||||
self._update_states_by_edit_mode()
|
||||
|
||||
def _get_url_user_edit(self):
|
||||
url_edit = True
|
||||
if self._logged_in and not self._url_edit_mode:
|
||||
url_edit = False
|
||||
user_edit = url_edit
|
||||
if not user_edit and self._logged_in and self._username_edit_mode:
|
||||
user_edit = True
|
||||
return url_edit, user_edit
|
||||
|
||||
def _update_states_by_edit_mode(self):
|
||||
url_edit, user_edit = self._get_url_user_edit()
|
||||
|
||||
self._url_preview.setVisible(not url_edit)
|
||||
self._url_input.setVisible(url_edit)
|
||||
self._url_edit_btn.setVisible(self._allow_logout and not url_edit)
|
||||
|
||||
self._username_preview.setVisible(not user_edit)
|
||||
self._username_input.setVisible(user_edit)
|
||||
self._username_edit_btn.setVisible(
|
||||
self._allow_logout and not user_edit
|
||||
)
|
||||
|
||||
self._api_preview.setVisible(not user_edit)
|
||||
self._api_label.setVisible(not user_edit)
|
||||
|
||||
self._password_label.setVisible(user_edit)
|
||||
self._show_password_btn.setVisible(user_edit)
|
||||
self._password_input.setVisible(user_edit)
|
||||
|
||||
self._logout_btn.setVisible(self._allow_logout and self._logged_in)
|
||||
self._login_btn.setVisible(not self._allow_logout)
|
||||
self._confirm_btn.setVisible(self._allow_logout)
|
||||
self._update_login_btn_state(url_edit, user_edit)
|
||||
|
||||
def _update_login_btn_state(self, url_edit=None, user_edit=None, url=None):
|
||||
if url_edit is None:
|
||||
url_edit, user_edit = self._get_url_user_edit()
|
||||
|
||||
if url is None:
|
||||
url = self._url_input.text()
|
||||
|
||||
enabled = bool(url) and (url_edit or user_edit)
|
||||
|
||||
self._login_btn.setEnabled(enabled)
|
||||
self._confirm_btn.setEnabled(enabled)
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
||||
def _on_first_show(self):
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
self.resize(self.default_width, self.default_height)
|
||||
self._center_window()
|
||||
if self._allow_logout is None:
|
||||
self.set_allow_logout(False)
|
||||
|
||||
self._update_states_by_edit_mode()
|
||||
if not self._url_input.text():
|
||||
widget = self._url_input
|
||||
elif not self._username_input.text():
|
||||
widget = self._username_input
|
||||
else:
|
||||
widget = self._password_input
|
||||
|
||||
self._set_input_focus(widget)
|
||||
|
||||
def result(self):
|
||||
"""Result url and token or login.
|
||||
|
||||
Returns:
|
||||
Union[Tuple[str, str], Tuple[None, None]]: Url and token used for
|
||||
login if was successful otherwise are both set to None.
|
||||
"""
|
||||
return self._result
|
||||
|
||||
def _center_window(self):
|
||||
"""Move window to center of screen."""
|
||||
|
||||
if hasattr(QtWidgets.QApplication, "desktop"):
|
||||
desktop = QtWidgets.QApplication.desktop()
|
||||
screen_idx = desktop.screenNumber(self)
|
||||
screen_geo = desktop.screenGeometry(screen_idx)
|
||||
else:
|
||||
screen = self.screen()
|
||||
screen_geo = screen.geometry()
|
||||
|
||||
geo = self.frameGeometry()
|
||||
geo.moveCenter(screen_geo.center())
|
||||
if geo.y() < screen_geo.y():
|
||||
geo.setY(screen_geo.y())
|
||||
self.move(geo.topLeft())
|
||||
|
||||
def _on_url_change(self, text):
|
||||
self._update_login_btn_state(url=text)
|
||||
self._set_url_valid(None)
|
||||
self._set_credentials_valid(None)
|
||||
self._url_preview.setText(text)
|
||||
|
||||
def _set_url_valid(self, valid):
|
||||
if valid is self._url_is_valid:
|
||||
return
|
||||
|
||||
self._url_is_valid = valid
|
||||
self._set_input_valid_state(self._url_input, valid)
|
||||
|
||||
def _set_credentials_valid(self, valid):
|
||||
if self._credentials_are_valid is valid:
|
||||
return
|
||||
|
||||
self._credentials_are_valid = valid
|
||||
self._set_input_valid_state(self._username_input, valid)
|
||||
self._set_input_valid_state(self._password_input, valid)
|
||||
|
||||
def _on_url_enter_press(self):
|
||||
self._set_input_focus(self._username_input)
|
||||
|
||||
def _on_user_change(self, username):
|
||||
self._username_preview.setText(username)
|
||||
|
||||
def _on_username_enter_press(self):
|
||||
self._set_input_focus(self._password_input)
|
||||
|
||||
def _on_password_enter_press(self):
|
||||
self._login()
|
||||
|
||||
def _on_show_password(self, show_password):
|
||||
if show_password:
|
||||
placeholder_text = "< MySecret124 >"
|
||||
echo_mode = QtWidgets.QLineEdit.Normal
|
||||
else:
|
||||
placeholder_text = "< *********** >"
|
||||
echo_mode = QtWidgets.QLineEdit.Password
|
||||
|
||||
self._password_input.setEchoMode(echo_mode)
|
||||
self._password_input.setPlaceholderText(placeholder_text)
|
||||
|
||||
def _on_username_edit_click(self):
|
||||
self._username_edit_mode = True
|
||||
self._update_states_by_edit_mode()
|
||||
|
||||
def _on_url_edit_click(self):
|
||||
self._url_edit_mode = True
|
||||
self._update_states_by_edit_mode()
|
||||
|
||||
def _on_logout_click(self):
|
||||
dialog = LogoutConfirmDialog(self)
|
||||
dialog.exec_()
|
||||
if dialog.get_result():
|
||||
self._result = (None, None, None, True)
|
||||
self.accept()
|
||||
|
||||
def _on_login_click(self):
|
||||
self._login()
|
||||
|
||||
def _validate_url(self):
|
||||
"""Use url from input to connect and change window state on success.
|
||||
|
||||
Todos:
|
||||
Threaded check.
|
||||
"""
|
||||
|
||||
url = self._url_input.text()
|
||||
valid_url = None
|
||||
try:
|
||||
valid_url = validate_url(url)
|
||||
|
||||
except UrlError as exc:
|
||||
parts = [f"<b>{exc.title}</b>"]
|
||||
parts.extend(f"- {hint}" for hint in exc.hints)
|
||||
self._set_message("<br/>".join(parts))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# Reraise KeyboardInterrupt error
|
||||
raise
|
||||
|
||||
except BaseException:
|
||||
self._set_unexpected_error()
|
||||
return
|
||||
|
||||
if valid_url is None:
|
||||
return False
|
||||
|
||||
self._url_input.setText(valid_url)
|
||||
return True
|
||||
|
||||
def _login(self):
|
||||
if (
|
||||
not self._login_btn.isEnabled()
|
||||
and not self._confirm_btn.isEnabled()
|
||||
):
|
||||
return
|
||||
|
||||
if not self._url_is_valid:
|
||||
self._set_url_valid(self._validate_url())
|
||||
|
||||
if not self._url_is_valid:
|
||||
self._set_input_focus(self._url_input)
|
||||
self._set_credentials_valid(None)
|
||||
return
|
||||
|
||||
self._clear_message()
|
||||
|
||||
url = self._url_input.text()
|
||||
username = self._username_input.text()
|
||||
password = self._password_input.text()
|
||||
try:
|
||||
token = login_to_server(url, username, password)
|
||||
except BaseException:
|
||||
self._set_unexpected_error()
|
||||
return
|
||||
|
||||
if token is not None:
|
||||
self._result = (url, token, username, False)
|
||||
self.accept()
|
||||
return
|
||||
|
||||
self._set_credentials_valid(False)
|
||||
message_lines = ["<b>Invalid credentials</b>"]
|
||||
if not username.strip():
|
||||
message_lines.append("- Username is not filled")
|
||||
|
||||
if not password.strip():
|
||||
message_lines.append("- Password is not filled")
|
||||
|
||||
if username and password:
|
||||
message_lines.append("- Check your credentials")
|
||||
|
||||
self._set_message("<br/>".join(message_lines))
|
||||
self._set_input_focus(self._username_input)
|
||||
|
||||
def _set_input_focus(self, widget):
|
||||
widget.setFocus(QtCore.Qt.MouseFocusReason)
|
||||
|
||||
def _set_input_valid_state(self, widget, valid):
|
||||
state = ""
|
||||
if valid is True:
|
||||
state = "valid"
|
||||
elif valid is False:
|
||||
state = "invalid"
|
||||
set_style_property(widget, "state", state)
|
||||
|
||||
def _set_message(self, message):
|
||||
self._message_label.setText(message)
|
||||
|
||||
def _clear_message(self):
|
||||
self._message_label.setText("")
|
||||
|
||||
def _set_unexpected_error(self):
|
||||
# TODO add traceback somewhere
|
||||
# - maybe a button to show or copy?
|
||||
traceback.print_exc()
|
||||
lines = [
|
||||
"<b>Unexpected error happened</b>",
|
||||
"- Can be caused by wrong url (leading elsewhere)"
|
||||
]
|
||||
self._set_message("<br/>".join(lines))
|
||||
|
||||
def set_url(self, url):
|
||||
self._url_preview.setText(url)
|
||||
self._url_input.setText(url)
|
||||
self._validate_url()
|
||||
|
||||
def set_username(self, username):
|
||||
self._username_preview.setText(username)
|
||||
self._username_input.setText(username)
|
||||
|
||||
def _set_api_key(self, api_key):
|
||||
if not api_key or len(api_key) < 3:
|
||||
self._api_preview.setText(api_key or "")
|
||||
return
|
||||
|
||||
api_key_len = len(api_key)
|
||||
offset = 6
|
||||
if api_key_len < offset:
|
||||
offset = api_key_len // 2
|
||||
api_key = api_key[:offset] + "." * (api_key_len - offset)
|
||||
|
||||
self._api_preview.setText(api_key)
|
||||
|
||||
def set_logged_in(
|
||||
self,
|
||||
logged_in,
|
||||
url=None,
|
||||
username=None,
|
||||
api_key=None,
|
||||
allow_logout=None
|
||||
):
|
||||
if url is not None:
|
||||
self.set_url(url)
|
||||
|
||||
if username is not None:
|
||||
self.set_username(username)
|
||||
|
||||
if api_key:
|
||||
self._set_api_key(api_key)
|
||||
|
||||
if logged_in and allow_logout is None:
|
||||
allow_logout = True
|
||||
|
||||
self._set_logged_in(logged_in)
|
||||
|
||||
if allow_logout:
|
||||
self.set_allow_logout(True)
|
||||
elif allow_logout is False:
|
||||
self.set_allow_logout(False)
|
||||
|
||||
|
||||
def ask_to_login(url=None, username=None, always_on_top=False):
|
||||
"""Ask user to login using Qt dialog.
|
||||
|
||||
Function creates new QApplication if is not created yet.
|
||||
|
||||
Args:
|
||||
url (Optional[str]): Server url that will be prefilled in dialog.
|
||||
username (Optional[str]): Username that will be prefilled in dialog.
|
||||
always_on_top (Optional[bool]): Window will be drawn on top of
|
||||
other windows.
|
||||
|
||||
Returns:
|
||||
tuple[str, str, str]: Returns Url, user's token and username. Url can
|
||||
be changed during dialog lifetime that's why the url is returned.
|
||||
"""
|
||||
|
||||
app_instance = get_qt_app()
|
||||
|
||||
window = ServerLoginWindow()
|
||||
if always_on_top:
|
||||
window.setWindowFlags(
|
||||
window.windowFlags()
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
|
||||
if url:
|
||||
window.set_url(url)
|
||||
|
||||
if username:
|
||||
window.set_username(username)
|
||||
|
||||
if not app_instance.startingUp():
|
||||
window.exec_()
|
||||
else:
|
||||
window.open()
|
||||
app_instance.exec_()
|
||||
result = window.result()
|
||||
out_url, out_token, out_username, _ = result
|
||||
return out_url, out_token, out_username
|
||||
|
||||
|
||||
def change_user(url, username, api_key, always_on_top=False):
|
||||
"""Ask user to login using Qt dialog.
|
||||
|
||||
Function creates new QApplication if is not created yet.
|
||||
|
||||
Args:
|
||||
url (str): Server url that will be prefilled in dialog.
|
||||
username (str): Username that will be prefilled in dialog.
|
||||
api_key (str): API key that will be prefilled in dialog.
|
||||
always_on_top (Optional[bool]): Window will be drawn on top of
|
||||
other windows.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: Returns Url and user's token. Url can be changed
|
||||
during dialog lifetime that's why the url is returned.
|
||||
"""
|
||||
|
||||
app_instance = get_qt_app()
|
||||
window = ServerLoginWindow()
|
||||
if always_on_top:
|
||||
window.setWindowFlags(
|
||||
window.windowFlags()
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
window.set_logged_in(True, url, username, api_key)
|
||||
|
||||
if not app_instance.startingUp():
|
||||
window.exec_()
|
||||
else:
|
||||
window.open()
|
||||
# This can become main Qt loop. Maybe should live elsewhere
|
||||
app_instance.exec_()
|
||||
return window.result()
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
|
||||
class PressHoverButton(QtWidgets.QPushButton):
|
||||
"""Keep track about mouse press/release and enter/leave."""
|
||||
|
||||
_mouse_pressed = False
|
||||
_mouse_hovered = False
|
||||
change_state = QtCore.Signal(bool)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self._mouse_pressed = True
|
||||
self._mouse_hovered = True
|
||||
self.change_state.emit(self._mouse_hovered)
|
||||
super(PressHoverButton, self).mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self._mouse_pressed = False
|
||||
self._mouse_hovered = False
|
||||
self.change_state.emit(self._mouse_hovered)
|
||||
super(PressHoverButton, self).mouseReleaseEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
|
||||
under_mouse = self.rect().contains(mouse_pos)
|
||||
if under_mouse != self._mouse_hovered:
|
||||
self._mouse_hovered = under_mouse
|
||||
self.change_state.emit(self._mouse_hovered)
|
||||
|
||||
super(PressHoverButton, self).mouseMoveEvent(event)
|
||||
|
||||
|
||||
class PlaceholderLineEdit(QtWidgets.QLineEdit):
|
||||
"""Set placeholder color of QLineEdit in Qt 5.12 and higher."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PlaceholderLineEdit, self).__init__(*args, **kwargs)
|
||||
# Change placeholder palette color
|
||||
if hasattr(QtGui.QPalette, "PlaceholderText"):
|
||||
filter_palette = self.palette()
|
||||
color = QtGui.QColor("#D3D8DE")
|
||||
color.setAlpha(67)
|
||||
filter_palette.setColor(
|
||||
QtGui.QPalette.PlaceholderText,
|
||||
color
|
||||
)
|
||||
self.setPalette(filter_palette)
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
Addon distribution tool
|
||||
------------------------
|
||||
|
||||
Code in this folder is backend portion of Addon distribution logic for v4 server.
|
||||
|
||||
Each host, module will be separate Addon in the future. Each v4 server could run different set of Addons.
|
||||
|
||||
Client (running on artist machine) will in the first step ask v4 for list of enabled addons.
|
||||
(It expects list of json documents matching to `addon_distribution.py:AddonInfo` object.)
|
||||
Next it will compare presence of enabled addon version in local folder. In the case of missing version of
|
||||
an addon, client will use information in the addon to download (from http/shared local disk/git) zip file
|
||||
and unzip it.
|
||||
|
||||
Required part of addon distribution will be sharing of dependencies (python libraries, utilities) which is not part of this folder.
|
||||
|
||||
Location of this folder might change in the future as it will be required for a clint to add this folder to sys.path reliably.
|
||||
|
||||
This code needs to be independent on Openpype code as much as possible!
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from .control import AyonDistribution, BundleNotFoundError
|
||||
from .utils import show_missing_bundle_information
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AyonDistribution",
|
||||
"BundleNotFoundError",
|
||||
"show_missing_bundle_information",
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,265 +0,0 @@
|
|||
import attr
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class UrlType(Enum):
|
||||
HTTP = "http"
|
||||
GIT = "git"
|
||||
FILESYSTEM = "filesystem"
|
||||
SERVER = "server"
|
||||
|
||||
|
||||
@attr.s
|
||||
class MultiPlatformValue(object):
|
||||
windows = attr.ib(default=None)
|
||||
linux = attr.ib(default=None)
|
||||
darwin = attr.ib(default=None)
|
||||
|
||||
|
||||
@attr.s
|
||||
class SourceInfo(object):
|
||||
type = attr.ib()
|
||||
|
||||
|
||||
@attr.s
|
||||
class LocalSourceInfo(SourceInfo):
|
||||
path = attr.ib(default=attr.Factory(MultiPlatformValue))
|
||||
|
||||
|
||||
@attr.s
|
||||
class WebSourceInfo(SourceInfo):
|
||||
url = attr.ib(default=None)
|
||||
headers = attr.ib(default=None)
|
||||
filename = attr.ib(default=None)
|
||||
|
||||
|
||||
@attr.s
|
||||
class ServerSourceInfo(SourceInfo):
|
||||
filename = attr.ib(default=None)
|
||||
path = attr.ib(default=None)
|
||||
|
||||
|
||||
def convert_source(source):
|
||||
"""Create source object from data information.
|
||||
|
||||
Args:
|
||||
source (Dict[str, any]): Information about source.
|
||||
|
||||
Returns:
|
||||
Union[None, SourceInfo]: Object with source information if type is
|
||||
known.
|
||||
"""
|
||||
|
||||
source_type = source.get("type")
|
||||
if not source_type:
|
||||
return None
|
||||
|
||||
if source_type == UrlType.FILESYSTEM.value:
|
||||
return LocalSourceInfo(
|
||||
type=source_type,
|
||||
path=source["path"]
|
||||
)
|
||||
|
||||
if source_type == UrlType.HTTP.value:
|
||||
url = source["path"]
|
||||
return WebSourceInfo(
|
||||
type=source_type,
|
||||
url=url,
|
||||
headers=source.get("headers"),
|
||||
filename=source.get("filename")
|
||||
)
|
||||
|
||||
if source_type == UrlType.SERVER.value:
|
||||
return ServerSourceInfo(
|
||||
type=source_type,
|
||||
filename=source.get("filename"),
|
||||
path=source.get("path")
|
||||
)
|
||||
|
||||
|
||||
def prepare_sources(src_sources):
|
||||
sources = []
|
||||
unknown_sources = []
|
||||
for source in (src_sources or []):
|
||||
dependency_source = convert_source(source)
|
||||
if dependency_source is not None:
|
||||
sources.append(dependency_source)
|
||||
else:
|
||||
print(f"Unknown source {source.get('type')}")
|
||||
unknown_sources.append(source)
|
||||
return sources, unknown_sources
|
||||
|
||||
|
||||
@attr.s
|
||||
class VersionData(object):
|
||||
version_data = attr.ib(default=None)
|
||||
|
||||
|
||||
@attr.s
|
||||
class AddonVersionInfo(object):
|
||||
version = attr.ib()
|
||||
full_name = attr.ib()
|
||||
title = attr.ib(default=None)
|
||||
require_distribution = attr.ib(default=False)
|
||||
sources = attr.ib(default=attr.Factory(list))
|
||||
unknown_sources = attr.ib(default=attr.Factory(list))
|
||||
hash = attr.ib(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_dict(
|
||||
cls, addon_name, addon_title, addon_version, version_data
|
||||
):
|
||||
"""Addon version info.
|
||||
|
||||
Args:
|
||||
addon_name (str): Name of addon.
|
||||
addon_title (str): Title of addon.
|
||||
addon_version (str): Version of addon.
|
||||
version_data (dict[str, Any]): Addon version information from
|
||||
server.
|
||||
|
||||
Returns:
|
||||
AddonVersionInfo: Addon version info.
|
||||
"""
|
||||
|
||||
full_name = f"{addon_name}_{addon_version}"
|
||||
title = f"{addon_title} {addon_version}"
|
||||
|
||||
source_info = version_data.get("clientSourceInfo")
|
||||
require_distribution = source_info is not None
|
||||
sources, unknown_sources = prepare_sources(source_info)
|
||||
|
||||
return cls(
|
||||
version=addon_version,
|
||||
full_name=full_name,
|
||||
require_distribution=require_distribution,
|
||||
sources=sources,
|
||||
unknown_sources=unknown_sources,
|
||||
hash=version_data.get("hash"),
|
||||
title=title
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class AddonInfo(object):
|
||||
"""Object matching json payload from Server"""
|
||||
name = attr.ib()
|
||||
versions = attr.ib(default=attr.Factory(dict))
|
||||
title = attr.ib(default=None)
|
||||
description = attr.ib(default=None)
|
||||
license = attr.ib(default=None)
|
||||
authors = attr.ib(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
"""Addon info by available versions.
|
||||
|
||||
Args:
|
||||
data (dict[str, Any]): Addon information from server. Should
|
||||
contain information about every version under 'versions'.
|
||||
|
||||
Returns:
|
||||
AddonInfo: Addon info with available versions.
|
||||
"""
|
||||
|
||||
# server payload contains info about all versions
|
||||
addon_name = data["name"]
|
||||
title = data.get("title") or addon_name
|
||||
|
||||
src_versions = data.get("versions") or {}
|
||||
dst_versions = {
|
||||
addon_version: AddonVersionInfo.from_dict(
|
||||
addon_name, title, addon_version, version_data
|
||||
)
|
||||
for addon_version, version_data in src_versions.items()
|
||||
}
|
||||
return cls(
|
||||
name=addon_name,
|
||||
versions=dst_versions,
|
||||
description=data.get("description"),
|
||||
title=data.get("title") or addon_name,
|
||||
license=data.get("license"),
|
||||
authors=data.get("authors")
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class DependencyItem(object):
|
||||
"""Object matching payload from Server about single dependency package"""
|
||||
name = attr.ib()
|
||||
platform_name = attr.ib()
|
||||
checksum = attr.ib()
|
||||
sources = attr.ib(default=attr.Factory(list))
|
||||
unknown_sources = attr.ib(default=attr.Factory(list))
|
||||
source_addons = attr.ib(default=attr.Factory(dict))
|
||||
python_modules = attr.ib(default=attr.Factory(dict))
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, package):
|
||||
src_sources = package.get("sources") or []
|
||||
for source in src_sources:
|
||||
if source.get("type") == "server" and not source.get("filename"):
|
||||
source["filename"] = package["filename"]
|
||||
sources, unknown_sources = prepare_sources(src_sources)
|
||||
return cls(
|
||||
name=package["filename"],
|
||||
platform_name=package["platform"],
|
||||
sources=sources,
|
||||
unknown_sources=unknown_sources,
|
||||
checksum=package["checksum"],
|
||||
source_addons=package["sourceAddons"],
|
||||
python_modules=package["pythonModules"]
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class Installer:
|
||||
version = attr.ib()
|
||||
filename = attr.ib()
|
||||
platform_name = attr.ib()
|
||||
size = attr.ib()
|
||||
checksum = attr.ib()
|
||||
python_version = attr.ib()
|
||||
python_modules = attr.ib()
|
||||
sources = attr.ib(default=attr.Factory(list))
|
||||
unknown_sources = attr.ib(default=attr.Factory(list))
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, installer_info):
|
||||
sources, unknown_sources = prepare_sources(
|
||||
installer_info.get("sources"))
|
||||
|
||||
return cls(
|
||||
version=installer_info["version"],
|
||||
filename=installer_info["filename"],
|
||||
platform_name=installer_info["platform"],
|
||||
size=installer_info["size"],
|
||||
sources=sources,
|
||||
unknown_sources=unknown_sources,
|
||||
checksum=installer_info["checksum"],
|
||||
python_version=installer_info["pythonVersion"],
|
||||
python_modules=installer_info["pythonModules"]
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class Bundle:
|
||||
"""Class representing bundle information."""
|
||||
|
||||
name = attr.ib()
|
||||
installer_version = attr.ib()
|
||||
addon_versions = attr.ib(default=attr.Factory(dict))
|
||||
dependency_packages = attr.ib(default=attr.Factory(dict))
|
||||
is_production = attr.ib(default=False)
|
||||
is_staging = attr.ib(default=False)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
return cls(
|
||||
name=data["name"],
|
||||
installer_version=data.get("installerVersion"),
|
||||
addon_versions=data.get("addons", {}),
|
||||
dependency_packages=data.get("dependencyPackages", {}),
|
||||
is_production=data["isProduction"],
|
||||
is_staging=data["isStaging"],
|
||||
)
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
import os
|
||||
import logging
|
||||
import platform
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import ayon_api
|
||||
|
||||
from .file_handler import RemoteFileHandler
|
||||
from .data_structures import UrlType
|
||||
|
||||
|
||||
class SourceDownloader(metaclass=ABCMeta):
|
||||
"""Abstract class for source downloader."""
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def download(cls, source, destination_dir, data, transfer_progress):
|
||||
"""Returns url of downloaded addon zip file.
|
||||
|
||||
Tranfer progress can be ignored, in that case file transfer won't
|
||||
be shown as 0-100% but as 'running'. First step should be to set
|
||||
destination content size and then add transferred chunk sizes.
|
||||
|
||||
Args:
|
||||
source (dict): {type:"http", "url":"https://} ...}
|
||||
destination_dir (str): local folder to unzip
|
||||
data (dict): More information about download content. Always have
|
||||
'type' key in.
|
||||
transfer_progress (ayon_api.TransferProgress): Progress of
|
||||
transferred (copy/download) content.
|
||||
|
||||
Returns:
|
||||
(str) local path to addon zip file
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def cleanup(cls, source, destination_dir, data):
|
||||
"""Cleanup files when distribution finishes or crashes.
|
||||
|
||||
Cleanup e.g. temporary files (downloaded zip) or other related stuff
|
||||
to downloader.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def check_hash(cls, addon_path, addon_hash, hash_type="sha256"):
|
||||
"""Compares 'hash' of downloaded 'addon_url' file.
|
||||
|
||||
Args:
|
||||
addon_path (str): Local path to addon file.
|
||||
addon_hash (str): Hash of downloaded file.
|
||||
hash_type (str): Type of hash.
|
||||
|
||||
Raises:
|
||||
ValueError if hashes doesn't match
|
||||
"""
|
||||
|
||||
if not os.path.exists(addon_path):
|
||||
raise ValueError(f"{addon_path} doesn't exist.")
|
||||
if not RemoteFileHandler.check_integrity(
|
||||
addon_path, addon_hash, hash_type=hash_type
|
||||
):
|
||||
raise ValueError(f"{addon_path} doesn't match expected hash.")
|
||||
|
||||
@classmethod
|
||||
def unzip(cls, addon_zip_path, destination_dir):
|
||||
"""Unzips local 'addon_zip_path' to 'destination'.
|
||||
|
||||
Args:
|
||||
addon_zip_path (str): local path to addon zip file
|
||||
destination_dir (str): local folder to unzip
|
||||
"""
|
||||
|
||||
RemoteFileHandler.unzip(addon_zip_path, destination_dir)
|
||||
os.remove(addon_zip_path)
|
||||
|
||||
|
||||
class OSDownloader(SourceDownloader):
|
||||
"""Downloader using files from file drive."""
|
||||
|
||||
@classmethod
|
||||
def download(cls, source, destination_dir, data, transfer_progress):
|
||||
# OS doesn't need to download, unzip directly
|
||||
addon_url = source["path"].get(platform.system().lower())
|
||||
if not os.path.exists(addon_url):
|
||||
raise ValueError(f"{addon_url} is not accessible")
|
||||
return addon_url
|
||||
|
||||
@classmethod
|
||||
def cleanup(cls, source, destination_dir, data):
|
||||
# Nothing to do - download does not copy anything
|
||||
pass
|
||||
|
||||
|
||||
class HTTPDownloader(SourceDownloader):
|
||||
"""Downloader using http or https protocol."""
|
||||
|
||||
CHUNK_SIZE = 100000
|
||||
|
||||
@staticmethod
|
||||
def get_filename(source):
|
||||
source_url = source["url"]
|
||||
filename = source.get("filename")
|
||||
if not filename:
|
||||
filename = os.path.basename(source_url)
|
||||
basename, ext = os.path.splitext(filename)
|
||||
allowed_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS)
|
||||
if ext.lower().lstrip(".") not in allowed_exts:
|
||||
filename = f"{basename}.zip"
|
||||
return filename
|
||||
|
||||
@classmethod
|
||||
def download(cls, source, destination_dir, data, transfer_progress):
|
||||
source_url = source["url"]
|
||||
cls.log.debug(f"Downloading {source_url} to {destination_dir}")
|
||||
headers = source.get("headers")
|
||||
filename = cls.get_filename(source)
|
||||
|
||||
# TODO use transfer progress
|
||||
RemoteFileHandler.download_url(
|
||||
source_url,
|
||||
destination_dir,
|
||||
filename,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
return os.path.join(destination_dir, filename)
|
||||
|
||||
@classmethod
|
||||
def cleanup(cls, source, destination_dir, data):
|
||||
filename = cls.get_filename(source)
|
||||
filepath = os.path.join(destination_dir, filename)
|
||||
if os.path.exists(filepath) and os.path.isfile(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
class AyonServerDownloader(SourceDownloader):
|
||||
"""Downloads static resource file from AYON Server.
|
||||
|
||||
Expects filled env var AYON_SERVER_URL.
|
||||
"""
|
||||
|
||||
CHUNK_SIZE = 8192
|
||||
|
||||
@classmethod
|
||||
def download(cls, source, destination_dir, data, transfer_progress):
|
||||
path = source["path"]
|
||||
filename = source["filename"]
|
||||
if path and not filename:
|
||||
filename = path.split("/")[-1]
|
||||
|
||||
cls.log.debug(f"Downloading {filename} to {destination_dir}")
|
||||
|
||||
_, ext = os.path.splitext(filename)
|
||||
ext = ext.lower().lstrip(".")
|
||||
valid_exts = set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS)
|
||||
if ext not in valid_exts:
|
||||
raise ValueError((
|
||||
f"Invalid file extension \"{ext}\"."
|
||||
f" Expected {', '.join(valid_exts)}"
|
||||
))
|
||||
|
||||
if path:
|
||||
filepath = os.path.join(destination_dir, filename)
|
||||
return ayon_api.download_file(
|
||||
path,
|
||||
filepath,
|
||||
chunk_size=cls.CHUNK_SIZE,
|
||||
progress=transfer_progress
|
||||
)
|
||||
|
||||
# dst_filepath = os.path.join(destination_dir, filename)
|
||||
if data["type"] == "dependency_package":
|
||||
return ayon_api.download_dependency_package(
|
||||
data["name"],
|
||||
destination_dir,
|
||||
filename,
|
||||
platform_name=data["platform"],
|
||||
chunk_size=cls.CHUNK_SIZE,
|
||||
progress=transfer_progress
|
||||
)
|
||||
|
||||
if data["type"] == "addon":
|
||||
return ayon_api.download_addon_private_file(
|
||||
data["name"],
|
||||
data["version"],
|
||||
filename,
|
||||
destination_dir,
|
||||
chunk_size=cls.CHUNK_SIZE,
|
||||
progress=transfer_progress
|
||||
)
|
||||
|
||||
raise ValueError(f"Unknown type to download \"{data['type']}\"")
|
||||
|
||||
@classmethod
|
||||
def cleanup(cls, source, destination_dir, data):
|
||||
filename = source["filename"]
|
||||
filepath = os.path.join(destination_dir, filename)
|
||||
if os.path.exists(filepath) and os.path.isfile(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
class DownloadFactory:
|
||||
"""Factory for downloaders."""
|
||||
|
||||
def __init__(self):
|
||||
self._downloaders = {}
|
||||
|
||||
def register_format(self, downloader_type, downloader):
|
||||
"""Register downloader for download type.
|
||||
|
||||
Args:
|
||||
downloader_type (UrlType): Type of source.
|
||||
downloader (SourceDownloader): Downloader which cares about
|
||||
download, hash check and unzipping.
|
||||
"""
|
||||
|
||||
self._downloaders[downloader_type.value] = downloader
|
||||
|
||||
def get_downloader(self, downloader_type):
|
||||
"""Registered downloader for type.
|
||||
|
||||
Args:
|
||||
downloader_type (UrlType): Type of source.
|
||||
|
||||
Returns:
|
||||
SourceDownloader: Downloader object which should care about file
|
||||
distribution.
|
||||
|
||||
Raises:
|
||||
ValueError: If type does not have registered downloader.
|
||||
"""
|
||||
|
||||
if downloader := self._downloaders.get(downloader_type):
|
||||
return downloader()
|
||||
raise ValueError(f"{downloader_type} not implemented")
|
||||
|
||||
|
||||
def get_default_download_factory():
|
||||
download_factory = DownloadFactory()
|
||||
download_factory.register_format(UrlType.FILESYSTEM, OSDownloader)
|
||||
download_factory.register_format(UrlType.HTTP, HTTPDownloader)
|
||||
download_factory.register_format(UrlType.SERVER, AyonServerDownloader)
|
||||
return download_factory
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
import copy
|
||||
import tempfile
|
||||
|
||||
|
||||
import attr
|
||||
import pytest
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", "..", ".."))
|
||||
sys.path.append(root_dir)
|
||||
|
||||
from common.ayon_common.distribution.downloaders import (
|
||||
DownloadFactory,
|
||||
OSDownloader,
|
||||
HTTPDownloader,
|
||||
)
|
||||
from common.ayon_common.distribution.control import (
|
||||
AyonDistribution,
|
||||
UpdateState,
|
||||
)
|
||||
from common.ayon_common.distribution.data_structures import (
|
||||
AddonInfo,
|
||||
UrlType,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def download_factory():
|
||||
addon_downloader = DownloadFactory()
|
||||
addon_downloader.register_format(UrlType.FILESYSTEM, OSDownloader)
|
||||
addon_downloader.register_format(UrlType.HTTP, HTTPDownloader)
|
||||
|
||||
yield addon_downloader
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def http_downloader(download_factory):
|
||||
yield download_factory.get_downloader(UrlType.HTTP.value)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_folder():
|
||||
yield tempfile.mkdtemp(prefix="ayon_test_")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_bundles():
|
||||
yield {
|
||||
"bundles": [
|
||||
{
|
||||
"name": "TestBundle",
|
||||
"createdAt": "2023-06-29T00:00:00.0+00:00",
|
||||
"installerVersion": None,
|
||||
"addons": {
|
||||
"slack": "1.0.0"
|
||||
},
|
||||
"dependencyPackages": {},
|
||||
"isProduction": True,
|
||||
"isStaging": False
|
||||
}
|
||||
],
|
||||
"productionBundle": "TestBundle",
|
||||
"stagingBundle": None
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_addon_info():
|
||||
yield {
|
||||
"name": "slack",
|
||||
"title": "Slack addon",
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
"hasSettings": True,
|
||||
"hasSiteSettings": False,
|
||||
"clientPyproject": {
|
||||
"tool": {
|
||||
"poetry": {
|
||||
"dependencies": {
|
||||
"nxtools": "^1.6",
|
||||
"orjson": "^3.6.7",
|
||||
"typer": "^0.4.1",
|
||||
"email-validator": "^1.1.3",
|
||||
"python": "^3.10",
|
||||
"fastapi": "^0.73.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"clientSourceInfo": [
|
||||
{
|
||||
"type": "http",
|
||||
"path": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing", # noqa
|
||||
"filename": "dummy.zip"
|
||||
},
|
||||
{
|
||||
"type": "filesystem",
|
||||
"path": {
|
||||
"windows": "P:/sources/some_file.zip",
|
||||
"linux": "/mnt/srv/sources/some_file.zip",
|
||||
"darwin": "/Volumes/srv/sources/some_file.zip"
|
||||
}
|
||||
}
|
||||
],
|
||||
"frontendScopes": {
|
||||
"project": {
|
||||
"sidebar": "hierarchy",
|
||||
}
|
||||
},
|
||||
"hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658" # noqa
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
|
||||
|
||||
def test_register(printer):
|
||||
download_factory = DownloadFactory()
|
||||
|
||||
assert len(download_factory._downloaders) == 0, "Contains registered"
|
||||
|
||||
download_factory.register_format(UrlType.FILESYSTEM, OSDownloader)
|
||||
assert len(download_factory._downloaders) == 1, "Should contain one"
|
||||
|
||||
|
||||
def test_get_downloader(printer, download_factory):
|
||||
assert download_factory.get_downloader(UrlType.FILESYSTEM.value), "Should find" # noqa
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
download_factory.get_downloader("unknown"), "Shouldn't find"
|
||||
|
||||
|
||||
def test_addon_info(printer, sample_addon_info):
|
||||
"""Tests parsing of expected payload from v4 server into AadonInfo."""
|
||||
valid_minimum = {
|
||||
"name": "slack",
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
"clientSourceInfo": [
|
||||
{
|
||||
"type": "filesystem",
|
||||
"path": {
|
||||
"windows": "P:/sources/some_file.zip",
|
||||
"linux": "/mnt/srv/sources/some_file.zip",
|
||||
"darwin": "/Volumes/srv/sources/some_file.zip"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert AddonInfo.from_dict(valid_minimum), "Missing required fields"
|
||||
|
||||
addon = AddonInfo.from_dict(sample_addon_info)
|
||||
assert addon, "Should be created"
|
||||
assert addon.name == "slack", "Incorrect name"
|
||||
assert "1.0.0" in addon.versions, "Version is not in versions"
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
assert addon["name"], "Dict approach not implemented"
|
||||
|
||||
addon_as_dict = attr.asdict(addon)
|
||||
assert addon_as_dict["name"], "Dict approach should work"
|
||||
|
||||
|
||||
def _get_dist_item(dist_items, name, version):
|
||||
final_dist_info = next(
|
||||
(
|
||||
dist_info
|
||||
for dist_info in dist_items
|
||||
if (
|
||||
dist_info["addon_name"] == name
|
||||
and dist_info["addon_version"] == version
|
||||
)
|
||||
),
|
||||
{}
|
||||
)
|
||||
return final_dist_info["dist_item"]
|
||||
|
||||
|
||||
def test_update_addon_state(
|
||||
printer, sample_addon_info, temp_folder, download_factory, sample_bundles
|
||||
):
|
||||
"""Tests possible cases of addon update."""
|
||||
|
||||
addon_version = list(sample_addon_info["versions"])[0]
|
||||
broken_addon_info = copy.deepcopy(sample_addon_info)
|
||||
|
||||
# Cause crash because of invalid hash
|
||||
broken_addon_info["versions"][addon_version]["hash"] = "brokenhash"
|
||||
distribution = AyonDistribution(
|
||||
addon_dirpath=temp_folder,
|
||||
dependency_dirpath=temp_folder,
|
||||
dist_factory=download_factory,
|
||||
addons_info=[broken_addon_info],
|
||||
dependency_packages_info=[],
|
||||
bundles_info=sample_bundles
|
||||
)
|
||||
distribution.distribute()
|
||||
dist_items = distribution.get_addon_dist_items()
|
||||
slack_dist_item = _get_dist_item(
|
||||
dist_items,
|
||||
sample_addon_info["name"],
|
||||
addon_version
|
||||
)
|
||||
slack_state = slack_dist_item.state
|
||||
assert slack_state == UpdateState.UPDATE_FAILED, (
|
||||
"Update should have failed because of wrong hash")
|
||||
|
||||
# Fix cache and validate if was updated
|
||||
distribution = AyonDistribution(
|
||||
addon_dirpath=temp_folder,
|
||||
dependency_dirpath=temp_folder,
|
||||
dist_factory=download_factory,
|
||||
addons_info=[sample_addon_info],
|
||||
dependency_packages_info=[],
|
||||
bundles_info=sample_bundles
|
||||
)
|
||||
distribution.distribute()
|
||||
dist_items = distribution.get_addon_dist_items()
|
||||
slack_dist_item = _get_dist_item(
|
||||
dist_items,
|
||||
sample_addon_info["name"],
|
||||
addon_version
|
||||
)
|
||||
assert slack_dist_item.state == UpdateState.UPDATED, (
|
||||
"Addon should have been updated")
|
||||
|
||||
# Is UPDATED without calling distribute
|
||||
distribution = AyonDistribution(
|
||||
addon_dirpath=temp_folder,
|
||||
dependency_dirpath=temp_folder,
|
||||
dist_factory=download_factory,
|
||||
addons_info=[sample_addon_info],
|
||||
dependency_packages_info=[],
|
||||
bundles_info=sample_bundles
|
||||
)
|
||||
dist_items = distribution.get_addon_dist_items()
|
||||
slack_dist_item = _get_dist_item(
|
||||
dist_items,
|
||||
sample_addon_info["name"],
|
||||
addon_version
|
||||
)
|
||||
assert slack_dist_item.state == UpdateState.UPDATED, (
|
||||
"Addon should already exist")
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
import sys
|
||||
|
||||
from qtpy import QtWidgets, QtGui
|
||||
|
||||
from ayon_common import is_staging_enabled
|
||||
from ayon_common.resources import (
|
||||
get_icon_path,
|
||||
load_stylesheet,
|
||||
)
|
||||
from ayon_common.ui_utils import get_qt_app
|
||||
|
||||
|
||||
class MissingBundleWindow(QtWidgets.QDialog):
|
||||
default_width = 410
|
||||
default_height = 170
|
||||
|
||||
def __init__(
|
||||
self, url=None, bundle_name=None, use_staging=None, parent=None
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
icon_path = get_icon_path()
|
||||
icon = QtGui.QIcon(icon_path)
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowTitle("Missing Bundle")
|
||||
|
||||
self._url = url
|
||||
self._bundle_name = bundle_name
|
||||
self._use_staging = use_staging
|
||||
self._first_show = True
|
||||
|
||||
info_label = QtWidgets.QLabel("", self)
|
||||
info_label.setWordWrap(True)
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
confirm_btn = QtWidgets.QPushButton("Exit", btns_widget)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(confirm_btn, 0)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.addWidget(info_label, 0)
|
||||
main_layout.addStretch(1)
|
||||
main_layout.addWidget(btns_widget, 0)
|
||||
|
||||
confirm_btn.clicked.connect(self._on_confirm_click)
|
||||
|
||||
self._info_label = info_label
|
||||
self._confirm_btn = confirm_btn
|
||||
|
||||
self._update_label()
|
||||
|
||||
def set_url(self, url):
|
||||
if url == self._url:
|
||||
return
|
||||
self._url = url
|
||||
self._update_label()
|
||||
|
||||
def set_bundle_name(self, bundle_name):
|
||||
if bundle_name == self._bundle_name:
|
||||
return
|
||||
self._bundle_name = bundle_name
|
||||
self._update_label()
|
||||
|
||||
def set_use_staging(self, use_staging):
|
||||
if self._use_staging == use_staging:
|
||||
return
|
||||
self._use_staging = use_staging
|
||||
self._update_label()
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._on_first_show()
|
||||
self._recalculate_sizes()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self._recalculate_sizes()
|
||||
|
||||
def _recalculate_sizes(self):
|
||||
hint = self._confirm_btn.sizeHint()
|
||||
new_width = max((hint.width(), hint.height() * 3))
|
||||
self._confirm_btn.setMinimumWidth(new_width)
|
||||
|
||||
def _on_first_show(self):
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
self.resize(self.default_width, self.default_height)
|
||||
|
||||
def _on_confirm_click(self):
|
||||
self.accept()
|
||||
self.close()
|
||||
|
||||
def _update_label(self):
|
||||
self._info_label.setText(self._get_label())
|
||||
|
||||
def _get_label(self):
|
||||
url_part = f" <b>{self._url}</b>" if self._url else ""
|
||||
|
||||
if self._bundle_name:
|
||||
return (
|
||||
f"Requested release bundle <b>{self._bundle_name}</b>"
|
||||
f" is not available on server{url_part}."
|
||||
"<br/><br/>Try to restart AYON desktop launcher. Please"
|
||||
" contact your administrator if issue persist."
|
||||
)
|
||||
mode = "staging" if self._use_staging else "production"
|
||||
return (
|
||||
f"No release bundle is set as {mode} on the AYON"
|
||||
f" server{url_part} so there is nothing to launch."
|
||||
"<br/><br/>Please contact your administrator"
|
||||
" to resolve the issue."
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Show message that server does not have set bundle to use.
|
||||
|
||||
It is possible to pass url as argument to show it in the message. To use
|
||||
this feature, pass `--url <url>` as argument to this script.
|
||||
"""
|
||||
|
||||
url = None
|
||||
bundle_name = None
|
||||
if "--url" in sys.argv:
|
||||
url_index = sys.argv.index("--url") + 1
|
||||
if url_index < len(sys.argv):
|
||||
url = sys.argv[url_index]
|
||||
|
||||
if "--bundle" in sys.argv:
|
||||
bundle_index = sys.argv.index("--bundle") + 1
|
||||
if bundle_index < len(sys.argv):
|
||||
bundle_name = sys.argv[bundle_index]
|
||||
|
||||
use_staging = is_staging_enabled()
|
||||
app = get_qt_app()
|
||||
window = MissingBundleWindow(url, bundle_name, use_staging)
|
||||
window.show()
|
||||
app.exec_()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import os
|
||||
import subprocess
|
||||
|
||||
from ayon_common.utils import get_ayon_appdirs, get_ayon_launch_args
|
||||
|
||||
|
||||
def get_local_dir(*subdirs):
|
||||
"""Get product directory in user's home directory.
|
||||
|
||||
Each user on machine have own local directory where are downloaded updates,
|
||||
addons etc.
|
||||
|
||||
Returns:
|
||||
str: Path to product local directory.
|
||||
"""
|
||||
|
||||
if not subdirs:
|
||||
raise ValueError("Must fill dir_name if nothing else provided!")
|
||||
|
||||
local_dir = get_ayon_appdirs(*subdirs)
|
||||
if not os.path.isdir(local_dir):
|
||||
try:
|
||||
os.makedirs(local_dir)
|
||||
except Exception: # TODO fix exception
|
||||
raise RuntimeError(f"Cannot create {local_dir}")
|
||||
|
||||
return local_dir
|
||||
|
||||
|
||||
def get_addons_dir():
|
||||
"""Directory where addon packages are stored.
|
||||
|
||||
Path to addons is defined using python module 'appdirs' which
|
||||
|
||||
The path is stored into environment variable 'AYON_ADDONS_DIR'.
|
||||
Value of environment variable can be overriden, but we highly recommended
|
||||
to use that option only for development purposes.
|
||||
|
||||
Returns:
|
||||
str: Path to directory where addons should be downloaded.
|
||||
"""
|
||||
|
||||
addons_dir = os.environ.get("AYON_ADDONS_DIR")
|
||||
if not addons_dir:
|
||||
addons_dir = get_local_dir("addons")
|
||||
os.environ["AYON_ADDONS_DIR"] = addons_dir
|
||||
return addons_dir
|
||||
|
||||
|
||||
def get_dependencies_dir():
|
||||
"""Directory where dependency packages are stored.
|
||||
|
||||
Path to addons is defined using python module 'appdirs' which
|
||||
|
||||
The path is stored into environment variable 'AYON_DEPENDENCIES_DIR'.
|
||||
Value of environment variable can be overriden, but we highly recommended
|
||||
to use that option only for development purposes.
|
||||
|
||||
Returns:
|
||||
str: Path to directory where dependency packages should be downloaded.
|
||||
"""
|
||||
|
||||
dependencies_dir = os.environ.get("AYON_DEPENDENCIES_DIR")
|
||||
if not dependencies_dir:
|
||||
dependencies_dir = get_local_dir("dependency_packages")
|
||||
os.environ["AYON_DEPENDENCIES_DIR"] = dependencies_dir
|
||||
return dependencies_dir
|
||||
|
||||
|
||||
def show_missing_bundle_information(url, bundle_name=None):
|
||||
"""Show missing bundle information window.
|
||||
|
||||
This function should be called when server does not have set bundle for
|
||||
production or staging, or when bundle that should be used is not available
|
||||
on server.
|
||||
|
||||
Using subprocess to show the dialog. Is blocking and is waiting until
|
||||
dialog is closed.
|
||||
|
||||
Args:
|
||||
url (str): Server url where bundle is not set.
|
||||
bundle_name (Optional[str]): Name of bundle that was not found.
|
||||
"""
|
||||
|
||||
ui_dir = os.path.join(os.path.dirname(__file__), "ui")
|
||||
script_path = os.path.join(ui_dir, "missing_bundle_window.py")
|
||||
args = get_ayon_launch_args(script_path, "--skip-bootstrap", "--url", url)
|
||||
if bundle_name:
|
||||
args.extend(["--bundle", bundle_name])
|
||||
subprocess.call(args)
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
|
|
@ -1,25 +0,0 @@
|
|||
import os
|
||||
|
||||
from ayon_common.utils import is_staging_enabled
|
||||
|
||||
RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def get_resource_path(*args):
|
||||
path_items = list(args)
|
||||
path_items.insert(0, RESOURCES_DIR)
|
||||
return os.path.sep.join(path_items)
|
||||
|
||||
|
||||
def get_icon_path():
|
||||
if is_staging_enabled():
|
||||
return get_resource_path("AYON_staging.png")
|
||||
return get_resource_path("AYON.png")
|
||||
|
||||
|
||||
def load_stylesheet():
|
||||
stylesheet_path = get_resource_path("stylesheet.css")
|
||||
|
||||
with open(stylesheet_path, "r") as stream:
|
||||
content = stream.read()
|
||||
return content
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -1,84 +0,0 @@
|
|||
* {
|
||||
font-size: 10pt;
|
||||
font-family: "Noto Sans";
|
||||
font-weight: 450;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QWidget {
|
||||
color: #D3D8DE;
|
||||
background: #2C313A;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
QWidget:disabled {
|
||||
color: #5b6779;
|
||||
}
|
||||
|
||||
QLabel {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
QPushButton {
|
||||
text-align:center center;
|
||||
border: 0px solid transparent;
|
||||
border-radius: 0.2em;
|
||||
padding: 3px 5px 3px 5px;
|
||||
background: #434a56;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background: rgba(168, 175, 189, 0.3);
|
||||
color: #F0F2F5;
|
||||
}
|
||||
|
||||
QPushButton:pressed {}
|
||||
|
||||
QPushButton:disabled {
|
||||
background: #434a56;
|
||||
}
|
||||
|
||||
QLineEdit {
|
||||
border: 1px solid #373D48;
|
||||
border-radius: 0.3em;
|
||||
background: #21252B;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
QLineEdit:disabled {
|
||||
background: #2C313A;
|
||||
}
|
||||
QLineEdit:hover {
|
||||
border-color: rgba(168, 175, 189, .3);
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border-color: rgb(92, 173, 214);
|
||||
}
|
||||
|
||||
QLineEdit[state="invalid"] {
|
||||
border-color: #AA5050;
|
||||
}
|
||||
|
||||
#Separator {
|
||||
background: rgba(75, 83, 98, 127);
|
||||
}
|
||||
|
||||
#PasswordBtn {
|
||||
border: none;
|
||||
padding: 0.1em;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#PasswordBtn:hover {
|
||||
background: #434a56;
|
||||
}
|
||||
|
||||
#LikeDisabledInput {
|
||||
background: #2C313A;
|
||||
}
|
||||
#LikeDisabledInput:hover {
|
||||
border-color: #373D48;
|
||||
}
|
||||
#LikeDisabledInput:focus {
|
||||
border-color: #373D48;
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import sys
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
||||
def set_style_property(widget, property_name, property_value):
|
||||
"""Set widget's property that may affect style.
|
||||
|
||||
Style of widget is polished if current property value is different.
|
||||
"""
|
||||
|
||||
cur_value = widget.property(property_name)
|
||||
if cur_value == property_value:
|
||||
return
|
||||
widget.setProperty(property_name, property_value)
|
||||
widget.style().polish(widget)
|
||||
|
||||
|
||||
def get_qt_app():
|
||||
app = QtWidgets.QApplication.instance()
|
||||
if app is not None:
|
||||
return app
|
||||
|
||||
for attr_name in (
|
||||
"AA_EnableHighDpiScaling",
|
||||
"AA_UseHighDpiPixmaps",
|
||||
):
|
||||
attr = getattr(QtCore.Qt, attr_name, None)
|
||||
if attr is not None:
|
||||
QtWidgets.QApplication.setAttribute(attr)
|
||||
|
||||
if hasattr(QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy"):
|
||||
QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||
QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||
)
|
||||
|
||||
return QtWidgets.QApplication(sys.argv)
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
import appdirs
|
||||
|
||||
IS_BUILT_APPLICATION = getattr(sys, "frozen", False)
|
||||
|
||||
|
||||
def get_ayon_appdirs(*args):
|
||||
"""Local app data directory of AYON client.
|
||||
|
||||
Args:
|
||||
*args (Iterable[str]): Subdirectories/files in local app data dir.
|
||||
|
||||
Returns:
|
||||
str: Path to directory/file in local app data dir.
|
||||
"""
|
||||
|
||||
return os.path.join(
|
||||
appdirs.user_data_dir("AYON", "Ynput"),
|
||||
*args
|
||||
)
|
||||
|
||||
|
||||
def is_staging_enabled():
|
||||
"""Check if staging is enabled.
|
||||
|
||||
Returns:
|
||||
bool: True if staging is enabled.
|
||||
"""
|
||||
|
||||
return os.getenv("AYON_USE_STAGING") == "1"
|
||||
|
||||
|
||||
def _create_local_site_id():
|
||||
"""Create a local site identifier.
|
||||
|
||||
Returns:
|
||||
str: Randomly generated site id.
|
||||
"""
|
||||
|
||||
from coolname import generate_slug
|
||||
|
||||
new_id = generate_slug(3)
|
||||
|
||||
print("Created local site id \"{}\"".format(new_id))
|
||||
|
||||
return new_id
|
||||
|
||||
|
||||
def get_local_site_id():
|
||||
"""Get local site identifier.
|
||||
|
||||
Site id is created if does not exist yet.
|
||||
|
||||
Returns:
|
||||
str: Site id.
|
||||
"""
|
||||
|
||||
# used for background syncing
|
||||
site_id = os.environ.get("AYON_SITE_ID")
|
||||
if site_id:
|
||||
return site_id
|
||||
|
||||
site_id_path = get_ayon_appdirs("site_id")
|
||||
if os.path.exists(site_id_path):
|
||||
with open(site_id_path, "r") as stream:
|
||||
site_id = stream.read()
|
||||
|
||||
if not site_id:
|
||||
site_id = _create_local_site_id()
|
||||
with open(site_id_path, "w") as stream:
|
||||
stream.write(site_id)
|
||||
return site_id
|
||||
|
||||
|
||||
def get_ayon_launch_args(*args):
|
||||
"""Launch arguments that can be used to launch ayon process.
|
||||
|
||||
Args:
|
||||
*args (str): Additional arguments.
|
||||
|
||||
Returns:
|
||||
list[str]: Launch arguments.
|
||||
"""
|
||||
|
||||
output = [sys.executable]
|
||||
if not IS_BUILT_APPLICATION:
|
||||
output.append(sys.argv[0])
|
||||
output.extend(args)
|
||||
return output
|
||||
|
|
@ -35,6 +35,29 @@ LOG_WARNING = 1
|
|||
LOG_ERROR = 3
|
||||
|
||||
|
||||
def sanitize_long_path(path):
|
||||
"""Sanitize long paths (260 characters) when on Windows.
|
||||
|
||||
Long paths are not capatible with ZipFile or reading a file, so we can
|
||||
shorten the path to use.
|
||||
|
||||
Args:
|
||||
path (str): path to either directory or file.
|
||||
|
||||
Returns:
|
||||
str: sanitized path
|
||||
"""
|
||||
if platform.system().lower() != "windows":
|
||||
return path
|
||||
path = os.path.abspath(path)
|
||||
|
||||
if path.startswith("\\\\"):
|
||||
path = "\\\\?\\UNC\\" + path[2:]
|
||||
else:
|
||||
path = "\\\\?\\" + path
|
||||
return path
|
||||
|
||||
|
||||
def sha256sum(filename):
|
||||
"""Calculate sha256 for content of the file.
|
||||
|
||||
|
|
@ -54,6 +77,13 @@ def sha256sum(filename):
|
|||
return h.hexdigest()
|
||||
|
||||
|
||||
class ZipFileLongPaths(ZipFile):
|
||||
def _extract_member(self, member, targetpath, pwd):
|
||||
return ZipFile._extract_member(
|
||||
self, member, sanitize_long_path(targetpath), pwd
|
||||
)
|
||||
|
||||
|
||||
class OpenPypeVersion(semver.VersionInfo):
|
||||
"""Class for storing information about OpenPype version.
|
||||
|
||||
|
|
@ -780,7 +810,7 @@ class BootstrapRepos:
|
|||
def _create_openpype_zip(self, zip_path: Path, openpype_path: Path) -> None:
|
||||
"""Pack repositories and OpenPype into zip.
|
||||
|
||||
We are using :mod:`zipfile` instead :meth:`shutil.make_archive`
|
||||
We are using :mod:`ZipFile` instead :meth:`shutil.make_archive`
|
||||
because we need to decide what file and directories to include in zip
|
||||
and what not. They are determined by :attr:`zip_filter` on file level
|
||||
and :attr:`openpype_filter` on top level directory in OpenPype
|
||||
|
|
@ -834,7 +864,7 @@ class BootstrapRepos:
|
|||
|
||||
checksums.append(
|
||||
(
|
||||
sha256sum(file.as_posix()),
|
||||
sha256sum(sanitize_long_path(file.as_posix())),
|
||||
file.resolve().relative_to(openpype_root)
|
||||
)
|
||||
)
|
||||
|
|
@ -958,7 +988,9 @@ class BootstrapRepos:
|
|||
if platform.system().lower() == "windows":
|
||||
file_name = file_name.replace("/", "\\")
|
||||
try:
|
||||
current = sha256sum((path / file_name).as_posix())
|
||||
current = sha256sum(
|
||||
sanitize_long_path((path / file_name).as_posix())
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return False, f"Missing file [ {file_name} ]"
|
||||
|
||||
|
|
@ -1270,7 +1302,7 @@ class BootstrapRepos:
|
|||
|
||||
# extract zip there
|
||||
self._print("Extracting zip to destination ...")
|
||||
with ZipFile(version.path, "r") as zip_ref:
|
||||
with ZipFileLongPaths(version.path, "r") as zip_ref:
|
||||
zip_ref.extractall(destination)
|
||||
|
||||
self._print(f"Installed as {version.path.stem}")
|
||||
|
|
@ -1386,7 +1418,7 @@ class BootstrapRepos:
|
|||
|
||||
# extract zip there
|
||||
self._print("extracting zip to destination ...")
|
||||
with ZipFile(openpype_version.path, "r") as zip_ref:
|
||||
with ZipFileLongPaths(openpype_version.path, "r") as zip_ref:
|
||||
self._progress_callback(75)
|
||||
zip_ref.extractall(destination)
|
||||
self._progress_callback(100)
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
import warnings
|
||||
import functools
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class ActionDeprecatedWarning(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
def deprecated(new_destination):
|
||||
"""Mark functions as deprecated.
|
||||
|
||||
It will result in a warning being emitted when the function is used.
|
||||
"""
|
||||
|
||||
func = None
|
||||
if callable(new_destination):
|
||||
func = new_destination
|
||||
new_destination = None
|
||||
|
||||
def _decorator(decorated_func):
|
||||
if new_destination is None:
|
||||
warning_message = (
|
||||
" Please check content of deprecated function to figure out"
|
||||
" possible replacement."
|
||||
)
|
||||
else:
|
||||
warning_message = " Please replace your usage with '{}'.".format(
|
||||
new_destination
|
||||
)
|
||||
|
||||
@functools.wraps(decorated_func)
|
||||
def wrapper(*args, **kwargs):
|
||||
warnings.simplefilter("always", ActionDeprecatedWarning)
|
||||
warnings.warn(
|
||||
(
|
||||
"Call to deprecated function '{}'"
|
||||
"\nFunction was moved or removed.{}"
|
||||
).format(decorated_func.__name__, warning_message),
|
||||
category=ActionDeprecatedWarning,
|
||||
stacklevel=4
|
||||
)
|
||||
return decorated_func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
if func is None:
|
||||
return _decorator
|
||||
return _decorator(func)
|
||||
|
||||
|
||||
@deprecated("openpype.pipeline.publish.get_errored_instances_from_context")
|
||||
def get_errored_instances_from_context(context, plugin=None):
|
||||
"""
|
||||
Deprecated:
|
||||
Since 3.14.* will be removed in 3.16.* or later.
|
||||
"""
|
||||
|
||||
from openpype.pipeline.publish import get_errored_instances_from_context
|
||||
|
||||
return get_errored_instances_from_context(context, plugin=plugin)
|
||||
|
||||
|
||||
@deprecated("openpype.pipeline.publish.get_errored_plugins_from_context")
|
||||
def get_errored_plugins_from_data(context):
|
||||
"""
|
||||
Deprecated:
|
||||
Since 3.14.* will be removed in 3.16.* or later.
|
||||
"""
|
||||
|
||||
from openpype.pipeline.publish import get_errored_plugins_from_context
|
||||
|
||||
return get_errored_plugins_from_context(context)
|
||||
|
||||
|
||||
class RepairAction(pyblish.api.Action):
|
||||
"""Repairs the action
|
||||
|
||||
To process the repairing this requires a static `repair(instance)` method
|
||||
is available on the plugin.
|
||||
|
||||
Deprecated:
|
||||
'RepairAction' and 'RepairContextAction' were moved to
|
||||
'openpype.pipeline.publish' please change you imports.
|
||||
There is no "reasonable" way hot mark these classes as deprecated
|
||||
to show warning of wrong import. Deprecated since 3.14.* will be
|
||||
removed in 3.16.*
|
||||
|
||||
"""
|
||||
label = "Repair"
|
||||
on = "failed" # This action is only available on a failed plug-in
|
||||
icon = "wrench" # Icon from Awesome Icon
|
||||
|
||||
def process(self, context, plugin):
|
||||
|
||||
if not hasattr(plugin, "repair"):
|
||||
raise RuntimeError("Plug-in does not have repair method.")
|
||||
|
||||
# Get the errored instances
|
||||
self.log.info("Finding failed instances..")
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
for instance in errored_instances:
|
||||
plugin.repair(instance)
|
||||
|
||||
|
||||
class RepairContextAction(pyblish.api.Action):
|
||||
"""Repairs the action
|
||||
|
||||
To process the repairing this requires a static `repair(instance)` method
|
||||
is available on the plugin.
|
||||
|
||||
Deprecated:
|
||||
'RepairAction' and 'RepairContextAction' were moved to
|
||||
'openpype.pipeline.publish' please change you imports.
|
||||
There is no "reasonable" way hot mark these classes as deprecated
|
||||
to show warning of wrong import. Deprecated since 3.14.* will be
|
||||
removed in 3.16.*
|
||||
|
||||
"""
|
||||
label = "Repair"
|
||||
on = "failed" # This action is only available on a failed plug-in
|
||||
|
||||
def process(self, context, plugin):
|
||||
|
||||
if not hasattr(plugin, "repair"):
|
||||
raise RuntimeError("Plug-in does not have repair method.")
|
||||
|
||||
# Get the errored instances
|
||||
self.log.info("Finding failed instances..")
|
||||
errored_plugins = get_errored_plugins_from_data(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
if plugin in errored_plugins:
|
||||
self.log.info("Attempting fix ...")
|
||||
plugin.repair(context)
|
||||
|
|
@ -5,6 +5,7 @@ import sys
|
|||
import code
|
||||
import click
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from .pype_commands import PypeCommands
|
||||
|
||||
|
||||
|
|
@ -46,7 +47,11 @@ def main(ctx):
|
|||
|
||||
if ctx.invoked_subcommand is None:
|
||||
# Print help if headless mode is used
|
||||
if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1":
|
||||
if AYON_SERVER_ENABLED:
|
||||
is_headless = os.getenv("AYON_HEADLESS_MODE") == "1"
|
||||
else:
|
||||
is_headless = os.getenv("OPENPYPE_HEADLESS_MODE") == "1"
|
||||
if is_headless:
|
||||
print(ctx.get_help())
|
||||
sys.exit(0)
|
||||
else:
|
||||
|
|
@ -57,6 +62,9 @@ def main(ctx):
|
|||
@click.option("-d", "--dev", is_flag=True, help="Settings in Dev mode")
|
||||
def settings(dev):
|
||||
"""Show Pype Settings UI."""
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
raise RuntimeError("AYON does not support 'settings' command.")
|
||||
PypeCommands().launch_settings_gui(dev)
|
||||
|
||||
|
||||
|
|
@ -110,6 +118,8 @@ def eventserver(ftrack_url,
|
|||
on linux and window service).
|
||||
"""
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
raise RuntimeError("AYON does not support 'eventserver' command.")
|
||||
PypeCommands().launch_eventservercli(
|
||||
ftrack_url,
|
||||
ftrack_user,
|
||||
|
|
@ -134,6 +144,10 @@ def webpublisherwebserver(executable, upload_dir, host=None, port=None):
|
|||
Expect "pype.club" user created on Ftrack.
|
||||
"""
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
raise RuntimeError(
|
||||
"AYON does not support 'webpublisherwebserver' command."
|
||||
)
|
||||
PypeCommands().launch_webpublisher_webservercli(
|
||||
upload_dir=upload_dir,
|
||||
executable=executable,
|
||||
|
|
@ -182,43 +196,10 @@ def publish(paths, targets, gui):
|
|||
PypeCommands.publish(list(paths), targets, gui)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("path")
|
||||
@click.option("-h", "--host", help="Host")
|
||||
@click.option("-u", "--user", help="User email address")
|
||||
@click.option("-p", "--project", help="Project")
|
||||
@click.option("-t", "--targets", help="Targets", default=None,
|
||||
multiple=True)
|
||||
def remotepublishfromapp(project, path, host, user=None, targets=None):
|
||||
"""Start CLI publishing.
|
||||
|
||||
Publish collects json from paths provided as an argument.
|
||||
More than one path is allowed.
|
||||
"""
|
||||
|
||||
PypeCommands.remotepublishfromapp(
|
||||
project, path, host, user, targets=targets
|
||||
)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("path")
|
||||
@click.option("-u", "--user", help="User email address")
|
||||
@click.option("-p", "--project", help="Project")
|
||||
@click.option("-t", "--targets", help="Targets", default=None,
|
||||
multiple=True)
|
||||
def remotepublish(project, path, user=None, targets=None):
|
||||
"""Start CLI publishing.
|
||||
|
||||
Publish collects json from paths provided as an argument.
|
||||
More than one path is allowed.
|
||||
"""
|
||||
|
||||
PypeCommands.remotepublish(project, path, user, targets=targets)
|
||||
|
||||
|
||||
@main.command(context_settings={"ignore_unknown_options": True})
|
||||
def projectmanager():
|
||||
if AYON_SERVER_ENABLED:
|
||||
raise RuntimeError("AYON does not support 'projectmanager' command.")
|
||||
PypeCommands().launch_project_manager()
|
||||
|
||||
|
||||
|
|
@ -316,12 +297,18 @@ def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant,
|
|||
persist, app_variant, timeout, setup_only)
|
||||
|
||||
|
||||
@main.command()
|
||||
@main.command(help="DEPRECATED - run sync server")
|
||||
@click.pass_context
|
||||
@click.option("-a", "--active_site", required=True,
|
||||
help="Name of active stie")
|
||||
def syncserver(active_site):
|
||||
help="Name of active site")
|
||||
def syncserver(ctx, active_site):
|
||||
"""Run sync site server in background.
|
||||
|
||||
Deprecated:
|
||||
This command is deprecated and will be removed in future versions.
|
||||
Use '~/openpype_console module sync_server syncservice' instead.
|
||||
|
||||
Details:
|
||||
Some Site Sync use cases need to expose site to another one.
|
||||
For example if majority of artists work in studio, they are not using
|
||||
SS at all, but if you want to expose published assets to 'studio' site
|
||||
|
|
@ -335,7 +322,12 @@ def syncserver(active_site):
|
|||
var OPENPYPE_LOCAL_ID set to 'active_site'.
|
||||
"""
|
||||
|
||||
PypeCommands().syncserver(active_site)
|
||||
if AYON_SERVER_ENABLED:
|
||||
raise RuntimeError("AYON does not support 'syncserver' command.")
|
||||
|
||||
from openpype.modules.sync_server.sync_server_module import (
|
||||
syncservice)
|
||||
ctx.invoke(syncservice, active_site=active_site)
|
||||
|
||||
|
||||
@main.command()
|
||||
|
|
@ -347,6 +339,8 @@ def repack_version(directory):
|
|||
recalculating file checksums. It will try to use version detected in
|
||||
directory name.
|
||||
"""
|
||||
if AYON_SERVER_ENABLED:
|
||||
raise RuntimeError("AYON does not support 'repack-version' command.")
|
||||
PypeCommands().repack_version(directory)
|
||||
|
||||
|
||||
|
|
@ -358,6 +352,9 @@ def repack_version(directory):
|
|||
"--dbonly", help="Store only Database data", default=False, is_flag=True)
|
||||
def pack_project(project, dirpath, dbonly):
|
||||
"""Create a package of project with all files and database dump."""
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
raise RuntimeError("AYON does not support 'pack-project' command.")
|
||||
PypeCommands().pack_project(project, dirpath, dbonly)
|
||||
|
||||
|
||||
|
|
@ -370,6 +367,8 @@ def pack_project(project, dirpath, dbonly):
|
|||
"--dbonly", help="Store only Database data", default=False, is_flag=True)
|
||||
def unpack_project(zipfile, root, dbonly):
|
||||
"""Create a package of project with all files and database dump."""
|
||||
if AYON_SERVER_ENABLED:
|
||||
raise RuntimeError("AYON does not support 'unpack-project' command.")
|
||||
PypeCommands().unpack_project(zipfile, root, dbonly)
|
||||
|
||||
|
||||
|
|
@ -384,9 +383,17 @@ def interactive():
|
|||
Executable 'openpype_gui' on Windows won't work.
|
||||
"""
|
||||
|
||||
from openpype.version import __version__
|
||||
if AYON_SERVER_ENABLED:
|
||||
version = os.environ["AYON_VERSION"]
|
||||
banner = (
|
||||
f"AYON launcher {version}\nPython {sys.version} on {sys.platform}"
|
||||
)
|
||||
else:
|
||||
from openpype.version import __version__
|
||||
|
||||
banner = f"OpenPype {__version__}\nPython {sys.version} on {sys.platform}"
|
||||
banner = (
|
||||
f"OpenPype {__version__}\nPython {sys.version} on {sys.platform}"
|
||||
)
|
||||
code.interact(banner)
|
||||
|
||||
|
||||
|
|
@ -395,11 +402,13 @@ def interactive():
|
|||
is_flag=True, default=False)
|
||||
def version(build):
|
||||
"""Print OpenPype version."""
|
||||
if AYON_SERVER_ENABLED:
|
||||
print(os.environ["AYON_VERSION"])
|
||||
return
|
||||
|
||||
from openpype.version import __version__
|
||||
from igniter.bootstrap_repos import BootstrapRepos, OpenPypeVersion
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
local_version = BootstrapRepos.get_version(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ from .mongo import (
|
|||
OpenPypeMongoConnection,
|
||||
get_project_database,
|
||||
get_project_connection,
|
||||
load_json_file,
|
||||
replace_project_documents,
|
||||
store_project_documents,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -17,4 +20,7 @@ __all__ = (
|
|||
"OpenPypeMongoConnection",
|
||||
"get_project_database",
|
||||
"get_project_connection",
|
||||
"load_json_file",
|
||||
"replace_project_documents",
|
||||
"store_project_documents",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -212,16 +212,12 @@ def _process_referenced_pipeline_result(result, link_type):
|
|||
continue
|
||||
|
||||
for output in sorted(outputs_recursive, key=lambda o: o["depth"]):
|
||||
output_links = output.get("data", {}).get("inputLinks")
|
||||
if not output_links and output["type"] != "hero_version":
|
||||
continue
|
||||
|
||||
# Leaf
|
||||
if output["_id"] not in correctly_linked_ids:
|
||||
continue
|
||||
|
||||
_filter_input_links(
|
||||
output_links,
|
||||
output.get("data", {}).get("inputLinks"),
|
||||
link_type,
|
||||
correctly_linked_ids
|
||||
)
|
||||
|
|
|
|||
|
|
@ -133,7 +133,6 @@ def _get_default_template_name(templates):
|
|||
def _template_replacements_to_v3(template):
|
||||
return (
|
||||
template
|
||||
.replace("{folder[name]}", "{asset}")
|
||||
.replace("{product[name]}", "{subset}")
|
||||
.replace("{product[type]}", "{family}")
|
||||
)
|
||||
|
|
@ -715,7 +714,6 @@ def convert_v4_representation_to_v3(representation):
|
|||
if "template" in output_data:
|
||||
output_data["template"] = (
|
||||
output_data["template"]
|
||||
.replace("{folder[name]}", "{asset}")
|
||||
.replace("{product[name]}", "{subset}")
|
||||
.replace("{product[type]}", "{family}")
|
||||
)
|
||||
|
|
@ -977,7 +975,6 @@ def convert_create_representation_to_v4(representation, con):
|
|||
representation_data = representation["data"]
|
||||
representation_data["template"] = (
|
||||
representation_data["template"]
|
||||
.replace("{asset}", "{folder[name]}")
|
||||
.replace("{subset}", "{product[name]}")
|
||||
.replace("{family}", "{product[type]}")
|
||||
)
|
||||
|
|
@ -1077,7 +1074,7 @@ def convert_update_folder_to_v4(project_name, asset_id, update_data, con):
|
|||
parent_id = None
|
||||
tasks = None
|
||||
new_data = {}
|
||||
attribs = {}
|
||||
attribs = full_update_data.pop("attrib", {})
|
||||
if "type" in update_data:
|
||||
new_update_data["active"] = update_data["type"] == "asset"
|
||||
|
||||
|
|
@ -1116,6 +1113,9 @@ def convert_update_folder_to_v4(project_name, asset_id, update_data, con):
|
|||
print("Folder has new data: {}".format(new_data))
|
||||
new_update_data["data"] = new_data
|
||||
|
||||
if attribs:
|
||||
new_update_data["attrib"] = attribs
|
||||
|
||||
if has_task_changes:
|
||||
raise ValueError("Task changes of folder are not implemented")
|
||||
|
||||
|
|
@ -1129,7 +1129,7 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con):
|
|||
full_update_data = _from_flat_dict(update_data)
|
||||
data = full_update_data.get("data")
|
||||
new_data = {}
|
||||
attribs = {}
|
||||
attribs = full_update_data.pop("attrib", {})
|
||||
if data:
|
||||
if "family" in data:
|
||||
family = data.pop("family")
|
||||
|
|
@ -1151,9 +1151,6 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con):
|
|||
elif value is not REMOVED_VALUE:
|
||||
new_data[key] = value
|
||||
|
||||
if attribs:
|
||||
new_update_data["attribs"] = attribs
|
||||
|
||||
if "name" in update_data:
|
||||
new_update_data["name"] = update_data["name"]
|
||||
|
||||
|
|
@ -1168,6 +1165,9 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con):
|
|||
new_update_data["folderId"] = update_data["parent"]
|
||||
|
||||
flat_data = _to_flat_dict(new_update_data)
|
||||
if attribs:
|
||||
flat_data["attrib"] = attribs
|
||||
|
||||
if new_data:
|
||||
print("Subset has new data: {}".format(new_data))
|
||||
flat_data["data"] = new_data
|
||||
|
|
@ -1182,7 +1182,7 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con):
|
|||
full_update_data = _from_flat_dict(update_data)
|
||||
data = full_update_data.get("data")
|
||||
new_data = {}
|
||||
attribs = {}
|
||||
attribs = full_update_data.pop("attrib", {})
|
||||
if data:
|
||||
if "author" in data:
|
||||
new_update_data["author"] = data.pop("author")
|
||||
|
|
@ -1199,9 +1199,6 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con):
|
|||
elif value is not REMOVED_VALUE:
|
||||
new_data[key] = value
|
||||
|
||||
if attribs:
|
||||
new_update_data["attribs"] = attribs
|
||||
|
||||
if "name" in update_data:
|
||||
new_update_data["version"] = update_data["name"]
|
||||
|
||||
|
|
@ -1216,6 +1213,9 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con):
|
|||
new_update_data["productId"] = update_data["parent"]
|
||||
|
||||
flat_data = _to_flat_dict(new_update_data)
|
||||
if attribs:
|
||||
flat_data["attrib"] = attribs
|
||||
|
||||
if new_data:
|
||||
print("Version has new data: {}".format(new_data))
|
||||
flat_data["data"] = new_data
|
||||
|
|
@ -1255,7 +1255,7 @@ def convert_update_representation_to_v4(
|
|||
data = full_update_data.get("data")
|
||||
|
||||
new_data = {}
|
||||
attribs = {}
|
||||
attribs = full_update_data.pop("attrib", {})
|
||||
if data:
|
||||
for key, value in data.items():
|
||||
if key in folder_attributes:
|
||||
|
|
@ -1266,7 +1266,6 @@ def convert_update_representation_to_v4(
|
|||
if "template" in attribs:
|
||||
attribs["template"] = (
|
||||
attribs["template"]
|
||||
.replace("{asset}", "{folder[name]}")
|
||||
.replace("{family}", "{product[type]}")
|
||||
.replace("{subset}", "{product[name]}")
|
||||
)
|
||||
|
|
@ -1313,6 +1312,9 @@ def convert_update_representation_to_v4(
|
|||
new_update_data["files"] = new_files
|
||||
|
||||
flat_data = _to_flat_dict(new_update_data)
|
||||
if attribs:
|
||||
flat_data["attrib"] = attribs
|
||||
|
||||
if new_data:
|
||||
print("Representation has new data: {}".format(new_data))
|
||||
flat_data["data"] = new_data
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
"""Cache of thumbnails downloaded from AYON server.
|
||||
|
||||
Thumbnails are cached to appdirs to predefined directory.
|
||||
|
||||
This should be moved to thumbnails logic in pipeline but because it would
|
||||
overflow OpenPype logic it's here for now.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import collections
|
||||
|
|
@ -10,7 +18,7 @@ FileInfo = collections.namedtuple(
|
|||
)
|
||||
|
||||
|
||||
class ThumbnailCache:
|
||||
class AYONThumbnailCache:
|
||||
"""Cache of thumbnails on local storage.
|
||||
|
||||
Thumbnails are cached to appdirs to predefined directory. Each project has
|
||||
|
|
@ -32,13 +40,14 @@ class ThumbnailCache:
|
|||
|
||||
# Lifetime of thumbnails (in seconds)
|
||||
# - default 3 days
|
||||
days_alive = 3 * 24 * 60 * 60
|
||||
days_alive = 3
|
||||
# Max size of thumbnail directory (in bytes)
|
||||
# - default 2 Gb
|
||||
max_filesize = 2 * 1024 * 1024 * 1024
|
||||
|
||||
def __init__(self, cleanup=True):
|
||||
self._thumbnails_dir = None
|
||||
self._days_alive_secs = self.days_alive * 24 * 60 * 60
|
||||
if cleanup:
|
||||
self.cleanup()
|
||||
|
||||
|
|
@ -50,6 +59,7 @@ class ThumbnailCache:
|
|||
"""
|
||||
|
||||
if self._thumbnails_dir is None:
|
||||
# TODO use generic function
|
||||
directory = appdirs.user_data_dir("AYON", "Ynput")
|
||||
self._thumbnails_dir = os.path.join(directory, "thumbnails")
|
||||
return self._thumbnails_dir
|
||||
|
|
@ -121,7 +131,7 @@ class ThumbnailCache:
|
|||
for filename in filenames:
|
||||
path = os.path.join(root, filename)
|
||||
modification_time = os.path.getmtime(path)
|
||||
if current_time - modification_time > self.days_alive:
|
||||
if current_time - modification_time > self._days_alive_secs:
|
||||
os.remove(path)
|
||||
|
||||
def _max_size_cleanup(self, thumbnails_dir):
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
|
||||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
||||
|
|
@ -13,8 +13,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
|
||||
# Execute after workfile template copy
|
||||
order = 10
|
||||
app_groups = [
|
||||
"3dsmax",
|
||||
app_groups = {
|
||||
"3dsmax", "adsk_3dsmax",
|
||||
"maya",
|
||||
"nuke",
|
||||
"nukex",
|
||||
|
|
@ -26,8 +26,9 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
"photoshop",
|
||||
"tvpaint",
|
||||
"substancepainter",
|
||||
"aftereffects"
|
||||
]
|
||||
"aftereffects",
|
||||
}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
if not self.data.get("start_last_workfile"):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import shutil
|
||||
from openpype.lib import PreLaunchHook
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from openpype.pipeline.workfile import (
|
||||
get_custom_workfile_template,
|
||||
get_custom_workfile_template_by_string_context
|
||||
|
|
@ -19,7 +19,8 @@ class CopyTemplateWorkfile(PreLaunchHook):
|
|||
|
||||
# Before `AddLastWorkfileToLaunchArgs`
|
||||
order = 0
|
||||
app_groups = ["blender", "photoshop", "tvpaint", "aftereffects"]
|
||||
app_groups = {"blender", "photoshop", "tvpaint", "aftereffects"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
"""Check if can copy template for context and do it if possible.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from openpype.pipeline.workfile import create_workdir_extra_folders
|
||||
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ class CreateWorkdirExtraFolders(PreLaunchHook):
|
|||
|
||||
# Execute after workfile template copy
|
||||
order = 15
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
if not self.application.is_host:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import subprocess
|
||||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class LaunchFoundryAppsWindows(PreLaunchHook):
|
||||
|
|
@ -13,8 +13,9 @@ class LaunchFoundryAppsWindows(PreLaunchHook):
|
|||
|
||||
# Should be as last hook because must change launch arguments to string
|
||||
order = 1000
|
||||
app_groups = ["nuke", "nukeassist", "nukex", "hiero", "nukestudio"]
|
||||
platforms = ["windows"]
|
||||
app_groups = {"nuke", "nukeassist", "nukex", "hiero", "nukestudio"}
|
||||
platforms = {"windows"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
# Change `creationflags` to CREATE_NEW_CONSOLE
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from openpype.client import get_project, get_asset_by_name
|
||||
from openpype.lib import (
|
||||
from openpype.lib.applications import (
|
||||
PreLaunchHook,
|
||||
EnvironmentPrepData,
|
||||
prepare_app_environments,
|
||||
|
|
@ -10,6 +10,7 @@ from openpype.pipeline import Anatomy
|
|||
|
||||
class GlobalHostDataHook(PreLaunchHook):
|
||||
order = -100
|
||||
launch_types = set()
|
||||
|
||||
def execute(self):
|
||||
"""Prepare global objects to `data` that will be used for sure."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class LaunchWithTerminal(PreLaunchHook):
|
||||
|
|
@ -12,7 +12,8 @@ class LaunchWithTerminal(PreLaunchHook):
|
|||
"""
|
||||
order = 1000
|
||||
|
||||
platforms = ["darwin"]
|
||||
platforms = {"darwin"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
executable = str(self.launch_context.executable)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import os
|
||||
|
||||
from openpype.lib import (
|
||||
from openpype.lib import get_openpype_execute_args
|
||||
from openpype.lib.applications import (
|
||||
get_non_python_host_kwargs,
|
||||
PreLaunchHook,
|
||||
get_openpype_execute_args
|
||||
LaunchTypes,
|
||||
)
|
||||
from openpype.lib.applications import get_non_python_host_kwargs
|
||||
|
||||
from openpype import PACKAGE_DIR as OPENPYPE_DIR
|
||||
|
||||
|
|
@ -16,9 +17,10 @@ class NonPythonHostHook(PreLaunchHook):
|
|||
python script which launch the host. For these cases it is necessary to
|
||||
prepend python (or openpype) executable and script path before application's.
|
||||
"""
|
||||
app_groups = ["harmony", "photoshop", "aftereffects"]
|
||||
app_groups = {"harmony", "photoshop", "aftereffects"}
|
||||
|
||||
order = 20
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
# Pop executable
|
||||
|
|
@ -54,4 +56,3 @@ class NonPythonHostHook(PreLaunchHook):
|
|||
|
||||
self.launch_context.kwargs = \
|
||||
get_non_python_host_kwargs(self.launch_context.kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook
|
||||
|
||||
from openpype.pipeline.colorspace import (
|
||||
get_imageio_config
|
||||
)
|
||||
from openpype.pipeline.colorspace import get_imageio_config
|
||||
from openpype.pipeline.template_data import get_template_data_with_names
|
||||
|
||||
|
||||
|
|
@ -10,7 +8,7 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
"""Set OCIO environment variable for hosts that use OpenColorIO."""
|
||||
|
||||
order = 0
|
||||
hosts = [
|
||||
hosts = {
|
||||
"substancepainter",
|
||||
"fusion",
|
||||
"blender",
|
||||
|
|
@ -20,8 +18,9 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
"maya",
|
||||
"nuke",
|
||||
"hiero",
|
||||
"resolve"
|
||||
]
|
||||
"resolve",
|
||||
}
|
||||
launch_types = set()
|
||||
|
||||
def execute(self):
|
||||
"""Hook entry method."""
|
||||
|
|
@ -39,7 +38,8 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
host_name=self.host_name,
|
||||
project_settings=self.data["project_settings"],
|
||||
anatomy_data=template_data,
|
||||
anatomy=self.data["anatomy"]
|
||||
anatomy=self.data["anatomy"],
|
||||
env=self.launch_context.env,
|
||||
)
|
||||
|
||||
if config_data:
|
||||
|
|
|
|||
|
|
@ -32,19 +32,26 @@ class HostDirmap(object):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, host_name, project_name, project_settings=None, sync_module=None
|
||||
self,
|
||||
host_name,
|
||||
project_name,
|
||||
project_settings=None,
|
||||
sync_module=None
|
||||
):
|
||||
self.host_name = host_name
|
||||
self.project_name = project_name
|
||||
self._project_settings = project_settings
|
||||
self._sync_module = sync_module # to limit reinit of Modules
|
||||
self._sync_module = sync_module
|
||||
# to limit reinit of Modules
|
||||
self._sync_module_discovered = sync_module is not None
|
||||
self._log = None
|
||||
|
||||
@property
|
||||
def sync_module(self):
|
||||
if self._sync_module is None:
|
||||
if not self._sync_module_discovered:
|
||||
self._sync_module_discovered = True
|
||||
manager = ModulesManager()
|
||||
self._sync_module = manager["sync_server"]
|
||||
self._sync_module = manager.get("sync_server")
|
||||
return self._sync_module
|
||||
|
||||
@property
|
||||
|
|
@ -151,21 +158,25 @@ class HostDirmap(object):
|
|||
"""
|
||||
project_name = self.project_name
|
||||
|
||||
sync_module = self.sync_module
|
||||
mapping = {}
|
||||
if (not self.sync_module.enabled or
|
||||
project_name not in self.sync_module.get_enabled_projects()):
|
||||
if (
|
||||
sync_module is None
|
||||
or not sync_module.enabled
|
||||
or project_name not in sync_module.get_enabled_projects()
|
||||
):
|
||||
return mapping
|
||||
|
||||
active_site = self.sync_module.get_local_normalized_site(
|
||||
self.sync_module.get_active_site(project_name))
|
||||
remote_site = self.sync_module.get_local_normalized_site(
|
||||
self.sync_module.get_remote_site(project_name))
|
||||
active_site = sync_module.get_local_normalized_site(
|
||||
sync_module.get_active_site(project_name))
|
||||
remote_site = sync_module.get_local_normalized_site(
|
||||
sync_module.get_remote_site(project_name))
|
||||
self.log.debug(
|
||||
"active {} - remote {}".format(active_site, remote_site)
|
||||
)
|
||||
|
||||
if active_site == "local" and active_site != remote_site:
|
||||
sync_settings = self.sync_module.get_sync_project_setting(
|
||||
sync_settings = sync_module.get_sync_project_setting(
|
||||
project_name,
|
||||
exclude_locals=False,
|
||||
cached=False)
|
||||
|
|
@ -179,7 +190,7 @@ class HostDirmap(object):
|
|||
self.log.debug("remote overrides {}".format(remote_overrides))
|
||||
|
||||
current_platform = platform.system().lower()
|
||||
remote_provider = self.sync_module.get_provider_for_site(
|
||||
remote_provider = sync_module.get_provider_for_site(
|
||||
project_name, remote_site
|
||||
)
|
||||
# dirmap has sense only with regular disk provider, in the workfile
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ class RenderCreator(Creator):
|
|||
create_allow_context_change = True
|
||||
|
||||
# Settings
|
||||
default_variants = []
|
||||
mark_for_review = True
|
||||
|
||||
def create(self, subset_name_from_ui, data, pre_create_data):
|
||||
|
|
@ -171,6 +170,10 @@ class RenderCreator(Creator):
|
|||
)
|
||||
|
||||
self.mark_for_review = plugin_settings["mark_for_review"]
|
||||
self.default_variants = plugin_settings.get(
|
||||
"default_variants",
|
||||
plugin_settings.get("defaults") or []
|
||||
)
|
||||
|
||||
def get_detail_description(self):
|
||||
return """Creator for Render instances
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class CloseAE(pyblish.api.ContextPlugin):
|
|||
active = True
|
||||
|
||||
hosts = ["aftereffects"]
|
||||
targets = ["remotepublish"]
|
||||
targets = ["automated"]
|
||||
|
||||
def process(self, context):
|
||||
self.log.info("CloseAE")
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import os
|
||||
import sys
|
||||
import six
|
||||
|
||||
from openpype.lib import (
|
||||
get_ffmpeg_tool_path,
|
||||
run_subprocess,
|
||||
)
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.aftereffects.api import get_stub
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from openpype.pipeline import get_current_asset_name, get_current_task_name
|
|||
from openpype.tools.utils import host_tools
|
||||
|
||||
from .workio import OpenFileCacher
|
||||
from . import pipeline
|
||||
|
||||
PREVIEW_COLLECTIONS: Dict = dict()
|
||||
|
||||
|
|
@ -344,6 +345,26 @@ class LaunchWorkFiles(LaunchQtApp):
|
|||
self._window.refresh()
|
||||
|
||||
|
||||
class SetFrameRange(bpy.types.Operator):
|
||||
bl_idname = "wm.ayon_set_frame_range"
|
||||
bl_label = "Set Frame Range"
|
||||
|
||||
def execute(self, context):
|
||||
data = pipeline.get_asset_data()
|
||||
pipeline.set_frame_range(data)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetResolution(bpy.types.Operator):
|
||||
bl_idname = "wm.ayon_set_resolution"
|
||||
bl_label = "Set Resolution"
|
||||
|
||||
def execute(self, context):
|
||||
data = pipeline.get_asset_data()
|
||||
pipeline.set_resolution(data)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class TOPBAR_MT_avalon(bpy.types.Menu):
|
||||
"""Avalon menu."""
|
||||
|
||||
|
|
@ -381,9 +402,11 @@ class TOPBAR_MT_avalon(bpy.types.Menu):
|
|||
layout.operator(LaunchManager.bl_idname, text="Manage...")
|
||||
layout.operator(LaunchLibrary.bl_idname, text="Library...")
|
||||
layout.separator()
|
||||
layout.operator(SetFrameRange.bl_idname, text="Set Frame Range")
|
||||
layout.operator(SetResolution.bl_idname, text="Set Resolution")
|
||||
layout.separator()
|
||||
layout.operator(LaunchWorkFiles.bl_idname, text="Work Files...")
|
||||
# TODO (jasper): maybe add 'Reload Pipeline', 'Set Frame Range' and
|
||||
# 'Set Resolution'?
|
||||
# TODO (jasper): maybe add 'Reload Pipeline'
|
||||
|
||||
|
||||
def draw_avalon_menu(self, context):
|
||||
|
|
@ -399,6 +422,8 @@ classes = [
|
|||
LaunchManager,
|
||||
LaunchLibrary,
|
||||
LaunchWorkFiles,
|
||||
SetFrameRange,
|
||||
SetResolution,
|
||||
TOPBAR_MT_avalon,
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -113,22 +113,21 @@ def message_window(title, message):
|
|||
_process_app_events()
|
||||
|
||||
|
||||
def set_start_end_frames():
|
||||
def get_asset_data():
|
||||
project_name = get_current_project_name()
|
||||
asset_name = get_current_asset_name()
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
|
||||
return asset_doc.get("data")
|
||||
|
||||
|
||||
def set_frame_range(data):
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Default scene settings
|
||||
frameStart = scene.frame_start
|
||||
frameEnd = scene.frame_end
|
||||
fps = scene.render.fps / scene.render.fps_base
|
||||
resolution_x = scene.render.resolution_x
|
||||
resolution_y = scene.render.resolution_y
|
||||
|
||||
# Check if settings are set
|
||||
data = asset_doc.get("data")
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
|
@ -139,26 +138,47 @@ def set_start_end_frames():
|
|||
frameEnd = data.get("frameEnd")
|
||||
if data.get("fps"):
|
||||
fps = data.get("fps")
|
||||
if data.get("resolutionWidth"):
|
||||
resolution_x = data.get("resolutionWidth")
|
||||
if data.get("resolutionHeight"):
|
||||
resolution_y = data.get("resolutionHeight")
|
||||
|
||||
scene.frame_start = frameStart
|
||||
scene.frame_end = frameEnd
|
||||
scene.render.fps = round(fps)
|
||||
scene.render.fps_base = round(fps) / fps
|
||||
|
||||
|
||||
def set_resolution(data):
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Default scene settings
|
||||
resolution_x = scene.render.resolution_x
|
||||
resolution_y = scene.render.resolution_y
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
if data.get("resolutionWidth"):
|
||||
resolution_x = data.get("resolutionWidth")
|
||||
if data.get("resolutionHeight"):
|
||||
resolution_y = data.get("resolutionHeight")
|
||||
|
||||
scene.render.resolution_x = resolution_x
|
||||
scene.render.resolution_y = resolution_y
|
||||
|
||||
|
||||
def on_new():
|
||||
set_start_end_frames()
|
||||
|
||||
project = os.environ.get("AVALON_PROJECT")
|
||||
settings = get_project_settings(project)
|
||||
settings = get_project_settings(project).get("blender")
|
||||
|
||||
unit_scale_settings = settings.get("blender").get("unit_scale_settings")
|
||||
set_resolution_startup = settings.get("set_resolution_startup")
|
||||
set_frames_startup = settings.get("set_frames_startup")
|
||||
|
||||
data = get_asset_data()
|
||||
|
||||
if set_resolution_startup:
|
||||
set_resolution(data)
|
||||
if set_frames_startup:
|
||||
set_frame_range(data)
|
||||
|
||||
unit_scale_settings = settings.get("unit_scale_settings")
|
||||
unit_scale_enabled = unit_scale_settings.get("enabled")
|
||||
if unit_scale_enabled:
|
||||
unit_scale = unit_scale_settings.get("base_file_unit_scale")
|
||||
|
|
@ -166,12 +186,20 @@ def on_new():
|
|||
|
||||
|
||||
def on_open():
|
||||
set_start_end_frames()
|
||||
|
||||
project = os.environ.get("AVALON_PROJECT")
|
||||
settings = get_project_settings(project)
|
||||
settings = get_project_settings(project).get("blender")
|
||||
|
||||
unit_scale_settings = settings.get("blender").get("unit_scale_settings")
|
||||
set_resolution_startup = settings.get("set_resolution_startup")
|
||||
set_frames_startup = settings.get("set_frames_startup")
|
||||
|
||||
data = get_asset_data()
|
||||
|
||||
if set_resolution_startup:
|
||||
set_resolution(data)
|
||||
if set_frames_startup:
|
||||
set_frame_range(data)
|
||||
|
||||
unit_scale_settings = settings.get("unit_scale_settings")
|
||||
unit_scale_enabled = unit_scale_settings.get("enabled")
|
||||
apply_on_opening = unit_scale_settings.get("apply_on_opening")
|
||||
if unit_scale_enabled and apply_on_opening:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from pathlib import Path
|
||||
|
||||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class AddPythonScriptToLaunchArgs(PreLaunchHook):
|
||||
|
|
@ -8,9 +8,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook):
|
|||
|
||||
# Append after file argument
|
||||
order = 15
|
||||
app_groups = [
|
||||
"blender",
|
||||
]
|
||||
app_groups = {"blender"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
if not self.launch_context.data.get("python_scripts"):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import os
|
|||
import re
|
||||
import subprocess
|
||||
from platform import system
|
||||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class InstallPySideToBlender(PreLaunchHook):
|
||||
|
|
@ -16,7 +16,8 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
blender's python packages.
|
||||
"""
|
||||
|
||||
app_groups = ["blender"]
|
||||
app_groups = {"blender"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
# Prelaunch hook is not crucial
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import subprocess
|
||||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class BlenderConsoleWindows(PreLaunchHook):
|
||||
|
|
@ -13,8 +13,9 @@ class BlenderConsoleWindows(PreLaunchHook):
|
|||
|
||||
# Should be as last hook because must change launch arguments to string
|
||||
order = 1000
|
||||
app_groups = ["blender"]
|
||||
platforms = ["windows"]
|
||||
app_groups = {"blender"}
|
||||
platforms = {"windows"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
# Change `creationflags` to CREATE_NEW_CONSOLE
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
camera = cameras[0].name
|
||||
self.log.debug(f"camera: {camera}")
|
||||
|
||||
focal_length = cameras[0].data.lens
|
||||
|
||||
# get isolate objects list from meshes instance members .
|
||||
isolate_objects = [
|
||||
obj
|
||||
|
|
@ -40,6 +42,10 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
|
||||
task = instance.context.data["task"]
|
||||
|
||||
# Store focal length in `burninDataMembers`
|
||||
burninData = instance.data.setdefault("burninDataMembers", {})
|
||||
burninData["focalLength"] = focal_length
|
||||
|
||||
instance.data.update({
|
||||
"subset": f"{task}Review",
|
||||
"review_camera": camera,
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ class ExtractABC(publish.Extractor):
|
|||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
context = bpy.context
|
||||
scene = context.scene
|
||||
view_layer = context.view_layer
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
|
@ -31,24 +29,25 @@ class ExtractABC(publish.Extractor):
|
|||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
asset_group = None
|
||||
active = None
|
||||
|
||||
for obj in instance:
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
# Set as active the asset group
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
asset_group = obj
|
||||
active = obj
|
||||
|
||||
context = plugin.create_blender_context(
|
||||
active=asset_group, selected=selected)
|
||||
active=active, selected=selected)
|
||||
|
||||
# We export the abc
|
||||
bpy.ops.wm.alembic_export(
|
||||
context,
|
||||
filepath=filepath,
|
||||
selected=True,
|
||||
flatten=False
|
||||
)
|
||||
with bpy.context.temp_override(**context):
|
||||
# We export the abc
|
||||
bpy.ops.wm.alembic_export(
|
||||
filepath=filepath,
|
||||
selected=True,
|
||||
flatten=False
|
||||
)
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
|
|
|
|||
73
openpype/hosts/blender/plugins/publish/extract_camera_abc.py
Normal file
73
openpype/hosts/blender/plugins/publish/extract_camera_abc.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import os
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractCameraABC(publish.Extractor):
|
||||
"""Extract camera as ABC."""
|
||||
|
||||
label = "Extract Camera (ABC)"
|
||||
hosts = ["blender"]
|
||||
families = ["camera"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
context = bpy.context
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
active = None
|
||||
|
||||
asset_group = None
|
||||
for obj in instance:
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
asset_group = obj
|
||||
break
|
||||
assert asset_group, "No asset group found"
|
||||
|
||||
# Need to cast to list because children is a tuple
|
||||
selected = list(asset_group.children)
|
||||
active = selected[0]
|
||||
|
||||
for obj in selected:
|
||||
obj.select_set(True)
|
||||
|
||||
context = plugin.create_blender_context(
|
||||
active=active, selected=selected)
|
||||
|
||||
with bpy.context.temp_override(**context):
|
||||
# We export the abc
|
||||
bpy.ops.wm.alembic_export(
|
||||
filepath=filepath,
|
||||
selected=True,
|
||||
flatten=True
|
||||
)
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'abc',
|
||||
'ext': 'abc',
|
||||
'files': filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
@ -9,7 +9,7 @@ from openpype.hosts.blender.api import plugin
|
|||
class ExtractCamera(publish.Extractor):
|
||||
"""Extract as the camera as FBX."""
|
||||
|
||||
label = "Extract Camera"
|
||||
label = "Extract Camera (FBX)"
|
||||
hosts = ["blender"]
|
||||
families = ["camera"]
|
||||
optional = True
|
||||
|
|
@ -2,20 +2,18 @@ import os
|
|||
import shutil
|
||||
import winreg
|
||||
import subprocess
|
||||
from openpype.lib import PreLaunchHook, get_openpype_execute_args
|
||||
from openpype.hosts.celaction import scripts
|
||||
|
||||
CELACTION_SCRIPTS_DIR = os.path.dirname(
|
||||
os.path.abspath(scripts.__file__)
|
||||
)
|
||||
from openpype.lib import get_openpype_execute_args
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from openpype.hosts.celaction import CELACTION_ROOT_DIR
|
||||
|
||||
|
||||
class CelactionPrelaunchHook(PreLaunchHook):
|
||||
"""
|
||||
Bootstrap celacion with pype
|
||||
"""
|
||||
app_groups = ["celaction"]
|
||||
platforms = ["windows"]
|
||||
app_groups = {"celaction"}
|
||||
platforms = {"windows"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
asset_doc = self.data["asset_doc"]
|
||||
|
|
@ -37,7 +35,9 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
winreg.KEY_ALL_ACCESS
|
||||
)
|
||||
|
||||
path_to_cli = os.path.join(CELACTION_SCRIPTS_DIR, "publish_cli.py")
|
||||
path_to_cli = os.path.join(
|
||||
CELACTION_ROOT_DIR, "scripts", "publish_cli.py"
|
||||
)
|
||||
subprocess_args = get_openpype_execute_args("run", path_to_cli)
|
||||
openpype_executable = subprocess_args.pop(0)
|
||||
workfile_settings = self.get_workfile_settings()
|
||||
|
|
@ -122,9 +122,8 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
if not os.path.exists(workfile_path):
|
||||
# TODO add ability to set different template workfile path via
|
||||
# settings
|
||||
openpype_celaction_dir = os.path.dirname(CELACTION_SCRIPTS_DIR)
|
||||
template_path = os.path.join(
|
||||
openpype_celaction_dir,
|
||||
CELACTION_ROOT_DIR,
|
||||
"resources",
|
||||
"celaction_template_scene.scn"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,13 +6,10 @@ import socket
|
|||
from pprint import pformat
|
||||
|
||||
from openpype.lib import (
|
||||
PreLaunchHook,
|
||||
get_openpype_username,
|
||||
run_subprocess,
|
||||
)
|
||||
from openpype.lib.applications import (
|
||||
ApplicationLaunchFailed
|
||||
)
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from openpype.hosts import flame as opflame
|
||||
|
||||
|
||||
|
|
@ -22,11 +19,12 @@ class FlamePrelaunch(PreLaunchHook):
|
|||
Will make sure flame_script_dirs are copied to user's folder defined
|
||||
in environment var FLAME_SCRIPT_DIR.
|
||||
"""
|
||||
app_groups = ["flame"]
|
||||
app_groups = {"flame"}
|
||||
permissions = 0o777
|
||||
|
||||
wtc_script_path = os.path.join(
|
||||
opflame.HOST_DIR, "api", "scripts", "wiretap_com.py")
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
icon = "search" # Icon from Awesome Icon
|
||||
|
||||
def process(self, context, plugin):
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
errored_instances = get_errored_instances_from_context(
|
||||
context,
|
||||
plugin=plugin,
|
||||
)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
|
|
@ -51,6 +53,7 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
names = set()
|
||||
for tool in invalid:
|
||||
flow.Select(tool, True)
|
||||
comp.SetActiveTool(tool)
|
||||
names.add(tool.Name)
|
||||
self.log.info(
|
||||
"Selecting invalid tools: %s" % ", ".join(sorted(names))
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@ import os
|
|||
import shutil
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
|
||||
from openpype.hosts.fusion import (
|
||||
FUSION_HOST_DIR,
|
||||
FUSION_VERSIONS_DICT,
|
||||
get_fusion_version,
|
||||
)
|
||||
from openpype.lib.applications import (
|
||||
PreLaunchHook,
|
||||
LaunchTypes,
|
||||
ApplicationLaunchFailed,
|
||||
)
|
||||
|
||||
|
||||
class FusionCopyPrefsPrelaunch(PreLaunchHook):
|
||||
|
|
@ -21,8 +25,9 @@ class FusionCopyPrefsPrelaunch(PreLaunchHook):
|
|||
Master.prefs is defined in openpype/hosts/fusion/deploy/fusion_shared.prefs
|
||||
"""
|
||||
|
||||
app_groups = ["fusion"]
|
||||
app_groups = {"fusion"}
|
||||
order = 2
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def get_fusion_profile_name(self, profile_version) -> str:
|
||||
# Returns 'Default', unless FUSION16_PROFILE is set
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import os
|
||||
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
|
||||
from openpype.lib.applications import (
|
||||
PreLaunchHook,
|
||||
LaunchTypes,
|
||||
ApplicationLaunchFailed,
|
||||
)
|
||||
from openpype.hosts.fusion import (
|
||||
FUSION_HOST_DIR,
|
||||
FUSION_VERSIONS_DICT,
|
||||
|
|
@ -17,8 +21,9 @@ class FusionPrelaunch(PreLaunchHook):
|
|||
Fusion 18 : Python 3.6 - 3.10
|
||||
"""
|
||||
|
||||
app_groups = ["fusion"]
|
||||
app_groups = {"fusion"}
|
||||
order = 1
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
# making sure python 3 is installed at provided path
|
||||
|
|
|
|||
|
|
@ -94,15 +94,14 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
|
||||
# Generate thumbnail.
|
||||
thumbnail_path = os.path.join(path, "thumbnail.png")
|
||||
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||
args = [
|
||||
ffmpeg_path,
|
||||
args = openpype.lib.get_ffmpeg_tool_args(
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-i", os.path.join(path, list(collections[0])[0]),
|
||||
"-vf", "scale=300:-1",
|
||||
"-vframes", "1",
|
||||
thumbnail_path
|
||||
]
|
||||
)
|
||||
process = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import os
|
|||
import pyblish.api
|
||||
|
||||
from openpype.lib import (
|
||||
get_oiio_tools_path,
|
||||
get_oiio_tool_args,
|
||||
run_subprocess,
|
||||
)
|
||||
from openpype.pipeline import publish
|
||||
|
|
@ -18,7 +18,7 @@ class ExtractFrames(publish.Extractor):
|
|||
movie_extensions = ["mov", "mp4"]
|
||||
|
||||
def process(self, instance):
|
||||
oiio_tool_path = get_oiio_tools_path()
|
||||
oiio_tool_args = get_oiio_tool_args("oiiotool")
|
||||
staging_dir = self.staging_dir(instance)
|
||||
output_template = os.path.join(staging_dir, instance.data["name"])
|
||||
sequence = instance.context.data["activeTimeline"]
|
||||
|
|
@ -36,7 +36,7 @@ class ExtractFrames(publish.Extractor):
|
|||
output_path = output_template
|
||||
output_path += ".{:04d}.{}".format(int(frame), output_ext)
|
||||
|
||||
args = [oiio_tool_path]
|
||||
args = list(oiio_tool_args)
|
||||
|
||||
ext = os.path.splitext(input_path)[1][1:]
|
||||
if ext in self.movie_extensions:
|
||||
|
|
|
|||
|
|
@ -167,9 +167,12 @@ class HoudiniCreatorBase(object):
|
|||
class HoudiniCreator(NewCreator, HoudiniCreatorBase):
|
||||
"""Base class for most of the Houdini creator plugins."""
|
||||
selected_nodes = []
|
||||
settings_name = None
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
try:
|
||||
self.selected_nodes = []
|
||||
|
||||
if pre_create_data.get("use_selection"):
|
||||
self.selected_nodes = hou.selectedNodes()
|
||||
|
||||
|
|
@ -292,3 +295,21 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase):
|
|||
|
||||
"""
|
||||
return [hou.ropNodeTypeCategory()]
|
||||
|
||||
def apply_settings(self, project_settings, system_settings):
|
||||
"""Method called on initialization of plugin to apply settings."""
|
||||
|
||||
settings_name = self.settings_name
|
||||
if settings_name is None:
|
||||
settings_name = self.__class__.__name__
|
||||
|
||||
settings = project_settings["houdini"]["create"]
|
||||
settings = settings.get(settings_name)
|
||||
if settings is None:
|
||||
self.log.debug(
|
||||
"No settings found for {}".format(self.__class__.__name__)
|
||||
)
|
||||
return
|
||||
|
||||
for key, value in settings.items():
|
||||
setattr(self, key, value)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class SetPath(PreLaunchHook):
|
||||
|
|
@ -6,7 +6,8 @@ class SetPath(PreLaunchHook):
|
|||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = ["houdini"]
|
||||
app_groups = {"houdini"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
workdir = self.launch_context.env.get("AVALON_WORKDIR", "")
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ class CreateArnoldAss(plugin.HoudiniCreator):
|
|||
label = "Arnold ASS"
|
||||
family = "ass"
|
||||
icon = "magic"
|
||||
defaults = ["Main"]
|
||||
|
||||
# Default extension: `.ass` or `.ass.gz`
|
||||
# however calling HoudiniCreator.create()
|
||||
# will override it by the value in the project settings
|
||||
ext = ".ass"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from openpype.hosts.houdini.api import plugin
|
||||
from openpype.lib import EnumDef
|
||||
from openpype.lib import EnumDef, BoolDef
|
||||
|
||||
|
||||
class CreateArnoldRop(plugin.HoudiniCreator):
|
||||
|
|
@ -9,7 +9,6 @@ class CreateArnoldRop(plugin.HoudiniCreator):
|
|||
label = "Arnold ROP"
|
||||
family = "arnold_rop"
|
||||
icon = "magic"
|
||||
defaults = ["master"]
|
||||
|
||||
# Default extension
|
||||
ext = "exr"
|
||||
|
|
@ -24,7 +23,7 @@ class CreateArnoldRop(plugin.HoudiniCreator):
|
|||
# Add chunk size attribute
|
||||
instance_data["chunkSize"] = 1
|
||||
# Submit for job publishing
|
||||
instance_data["farm"] = True
|
||||
instance_data["farm"] = pre_create_data.get("farm")
|
||||
|
||||
instance = super(CreateArnoldRop, self).create(
|
||||
subset_name,
|
||||
|
|
@ -64,6 +63,9 @@ class CreateArnoldRop(plugin.HoudiniCreator):
|
|||
]
|
||||
|
||||
return attrs + [
|
||||
BoolDef("farm",
|
||||
label="Submitting to Farm",
|
||||
default=True),
|
||||
EnumDef("image_format",
|
||||
image_format_enum,
|
||||
default=self.ext,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from openpype.lib import EnumDef
|
|||
class CreateBGEO(plugin.HoudiniCreator):
|
||||
"""BGEO pointcache creator."""
|
||||
identifier = "io.openpype.creators.houdini.bgeo"
|
||||
label = "BGEO PointCache"
|
||||
label = "PointCache (Bgeo)"
|
||||
family = "pointcache"
|
||||
icon = "gears"
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ class CreateKarmaROP(plugin.HoudiniCreator):
|
|||
label = "Karma ROP"
|
||||
family = "karma_rop"
|
||||
icon = "magic"
|
||||
defaults = ["master"]
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
import hou # noqa
|
||||
|
|
@ -21,7 +20,7 @@ class CreateKarmaROP(plugin.HoudiniCreator):
|
|||
# Add chunk size attribute
|
||||
instance_data["chunkSize"] = 10
|
||||
# Submit for job publishing
|
||||
instance_data["farm"] = True
|
||||
instance_data["farm"] = pre_create_data.get("farm")
|
||||
|
||||
instance = super(CreateKarmaROP, self).create(
|
||||
subset_name,
|
||||
|
|
@ -67,6 +66,7 @@ class CreateKarmaROP(plugin.HoudiniCreator):
|
|||
camera = None
|
||||
for node in self.selected_nodes:
|
||||
if node.type().name() == "cam":
|
||||
camera = node.path()
|
||||
has_camera = pre_create_data.get("cam_res")
|
||||
if has_camera:
|
||||
res_x = node.evalParm("resx")
|
||||
|
|
@ -96,6 +96,9 @@ class CreateKarmaROP(plugin.HoudiniCreator):
|
|||
]
|
||||
|
||||
return attrs + [
|
||||
BoolDef("farm",
|
||||
label="Submitting to Farm",
|
||||
default=True),
|
||||
EnumDef("image_format",
|
||||
image_format_enum,
|
||||
default="exr",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ class CreateMantraROP(plugin.HoudiniCreator):
|
|||
label = "Mantra ROP"
|
||||
family = "mantra_rop"
|
||||
icon = "magic"
|
||||
defaults = ["master"]
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
import hou # noqa
|
||||
|
|
@ -21,7 +20,7 @@ class CreateMantraROP(plugin.HoudiniCreator):
|
|||
# Add chunk size attribute
|
||||
instance_data["chunkSize"] = 10
|
||||
# Submit for job publishing
|
||||
instance_data["farm"] = True
|
||||
instance_data["farm"] = pre_create_data.get("farm")
|
||||
|
||||
instance = super(CreateMantraROP, self).create(
|
||||
subset_name,
|
||||
|
|
@ -76,6 +75,9 @@ class CreateMantraROP(plugin.HoudiniCreator):
|
|||
]
|
||||
|
||||
return attrs + [
|
||||
BoolDef("farm",
|
||||
label="Submitting to Farm",
|
||||
default=True),
|
||||
EnumDef("image_format",
|
||||
image_format_enum,
|
||||
default="exr",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import hou
|
|||
class CreatePointCache(plugin.HoudiniCreator):
|
||||
"""Alembic ROP to pointcache"""
|
||||
identifier = "io.openpype.creators.houdini.pointcache"
|
||||
label = "Point Cache"
|
||||
label = "PointCache (Abc)"
|
||||
family = "pointcache"
|
||||
icon = "gears"
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator):
|
|||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
parms = {
|
||||
"RS_archive_file": '$HIP/pyblish/`{}.$F4.rs'.format(subset_name),
|
||||
"RS_archive_file": '$HIP/pyblish/{}.$F4.rs'.format(subset_name),
|
||||
}
|
||||
|
||||
if self.selected_nodes:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import hou # noqa
|
||||
|
||||
from openpype.hosts.houdini.api import plugin
|
||||
from openpype.lib import EnumDef
|
||||
from openpype.lib import EnumDef, BoolDef
|
||||
|
||||
|
||||
class CreateRedshiftROP(plugin.HoudiniCreator):
|
||||
|
|
@ -13,7 +13,6 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
label = "Redshift ROP"
|
||||
family = "redshift_rop"
|
||||
icon = "magic"
|
||||
defaults = ["master"]
|
||||
ext = "exr"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
|
|
@ -23,7 +22,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
# Add chunk size attribute
|
||||
instance_data["chunkSize"] = 10
|
||||
# Submit for job publishing
|
||||
instance_data["farm"] = True
|
||||
instance_data["farm"] = pre_create_data.get("farm")
|
||||
|
||||
instance = super(CreateRedshiftROP, self).create(
|
||||
subset_name,
|
||||
|
|
@ -100,6 +99,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
]
|
||||
|
||||
return attrs + [
|
||||
BoolDef("farm",
|
||||
label="Submitting to Farm",
|
||||
default=True),
|
||||
EnumDef("image_format",
|
||||
image_format_enum,
|
||||
default=self.ext,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ class CreateVrayROP(plugin.HoudiniCreator):
|
|||
label = "VRay ROP"
|
||||
family = "vray_rop"
|
||||
icon = "magic"
|
||||
defaults = ["master"]
|
||||
|
||||
ext = "exr"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
|
|
@ -25,7 +23,7 @@ class CreateVrayROP(plugin.HoudiniCreator):
|
|||
# Add chunk size attribute
|
||||
instance_data["chunkSize"] = 10
|
||||
# Submit for job publishing
|
||||
instance_data["farm"] = True
|
||||
instance_data["farm"] = pre_create_data.get("farm")
|
||||
|
||||
instance = super(CreateVrayROP, self).create(
|
||||
subset_name,
|
||||
|
|
@ -139,6 +137,9 @@ class CreateVrayROP(plugin.HoudiniCreator):
|
|||
]
|
||||
|
||||
return attrs + [
|
||||
BoolDef("farm",
|
||||
label="Submitting to Farm",
|
||||
default=True),
|
||||
EnumDef("image_format",
|
||||
image_format_enum,
|
||||
default=self.ext,
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@ class CollectPointcacheType(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
if instance.data["creator_identifier"] == "io.openpype.creators.houdini.bgeo": # noqa: E501
|
||||
instance.data["families"] += ["bgeo"]
|
||||
elif instance.data["creator_identifier"] == "io.openpype.creators.houdini.alembic": # noqa: E501
|
||||
elif instance.data["creator_identifier"] == "io.openpype.creators.houdini.pointcache": # noqa: E501
|
||||
instance.data["families"] += ["abc"]
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validator plugin for SOP Path in bgeo isntance."""
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
|
||||
class ValidateNoSOPPath(pyblish.api.InstancePlugin):
|
||||
"""Validate if SOP Path in BGEO instance exists."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["bgeo"]
|
||||
label = "Validate BGEO SOP Path"
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
import hou
|
||||
|
||||
node = hou.node(instance.data.get("instance_node"))
|
||||
sop_path = node.evalParm("soppath")
|
||||
if not sop_path:
|
||||
raise PublishValidationError(
|
||||
("Empty SOP Path ('soppath' parameter) found in "
|
||||
f"the BGEO instance Geometry - {node.path()}"))
|
||||
if not isinstance(hou.node(sop_path), hou.SopNode):
|
||||
raise PublishValidationError(
|
||||
"SOP path is not pointing to valid SOP node.")
|
||||
|
|
@ -32,8 +32,9 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
nodes = [n.path() for n in invalid]
|
||||
raise PublishValidationError(
|
||||
"See log for details. " "Invalid nodes: {0}".format(invalid),
|
||||
"See log for details. " "Invalid nodes: {0}".format(nodes),
|
||||
title=self.label
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ from openpype.pipeline import (
|
|||
)
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
|
||||
|
||||
class ValidateWorkfilePaths(
|
||||
pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData"
|
|||
temp_arr = #()
|
||||
for x in all_handles do
|
||||
(
|
||||
if x.node == undefined do continue
|
||||
handle_name = node_to_name x.node
|
||||
append temp_arr handle_name
|
||||
)
|
||||
|
|
@ -185,7 +186,10 @@ class MaxCreatorBase(object):
|
|||
node = rt.Container(name=node)
|
||||
|
||||
attrs = rt.Execute(MS_CUSTOM_ATTRIB)
|
||||
rt.custAttributes.add(node.baseObject, attrs)
|
||||
modifier = rt.EmptyModifier()
|
||||
rt.addModifier(node, modifier)
|
||||
node.modifiers[0].name = "OP Data"
|
||||
rt.custAttributes.add(node.modifiers[0], attrs)
|
||||
|
||||
return node
|
||||
|
||||
|
|
@ -209,13 +213,19 @@ class MaxCreator(Creator, MaxCreatorBase):
|
|||
if pre_create_data.get("use_selection"):
|
||||
|
||||
node_list = []
|
||||
sel_list = []
|
||||
for i in self.selected_nodes:
|
||||
node_ref = rt.NodeTransformMonitor(node=i)
|
||||
node_list.append(node_ref)
|
||||
sel_list.append(str(i))
|
||||
|
||||
# Setting the property
|
||||
rt.setProperty(
|
||||
instance_node.openPypeData, "all_handles", node_list)
|
||||
instance_node.modifiers[0].openPypeData,
|
||||
"all_handles", node_list)
|
||||
rt.setProperty(
|
||||
instance_node.modifiers[0].openPypeData,
|
||||
"sel_list", sel_list)
|
||||
|
||||
self._add_instance_to_context(instance)
|
||||
imprint(instance_node.name, instance.data_to_store())
|
||||
|
|
@ -254,8 +264,8 @@ class MaxCreator(Creator, MaxCreatorBase):
|
|||
instance_node = rt.GetNodeByName(
|
||||
instance.data.get("instance_node"))
|
||||
if instance_node:
|
||||
count = rt.custAttributes.count(instance_node)
|
||||
rt.custAttributes.delete(instance_node, count)
|
||||
count = rt.custAttributes.count(instance_node.modifiers[0])
|
||||
rt.custAttributes.delete(instance_node.modifiers[0], count)
|
||||
rt.Delete(instance_node)
|
||||
|
||||
self._remove_instance_from_context(instance)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pre-launch to force 3ds max startup script."""
|
||||
from openpype.lib import PreLaunchHook
|
||||
import os
|
||||
from openpype.hosts.max import MAX_HOST_DIR
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class ForceStartupScript(PreLaunchHook):
|
||||
|
|
@ -13,12 +14,14 @@ class ForceStartupScript(PreLaunchHook):
|
|||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = ["3dsmax"]
|
||||
app_groups = {"3dsmax", "adsk_3dsmax"}
|
||||
order = 11
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
startup_args = [
|
||||
"-U",
|
||||
"MAXScript",
|
||||
f"{os.getenv('OPENPYPE_ROOT')}\\openpype\\hosts\\max\\startup\\startup.ms"] # noqa
|
||||
os.path.join(MAX_HOST_DIR, "startup", "startup.ms"),
|
||||
]
|
||||
self.launch_context.launch_args.append(startup_args)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pre-launch hook to inject python environment."""
|
||||
from openpype.lib import PreLaunchHook
|
||||
import os
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class InjectPythonPath(PreLaunchHook):
|
||||
|
|
@ -13,7 +13,8 @@ class InjectPythonPath(PreLaunchHook):
|
|||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = ["3dsmax"]
|
||||
app_groups = {"3dsmax", "adsk_3dsmax"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
self.launch_context.env["MAX_PYTHONPATH"] = os.environ["PYTHONPATH"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class SetPath(PreLaunchHook):
|
||||
|
|
@ -6,7 +6,8 @@ class SetPath(PreLaunchHook):
|
|||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = ["max"]
|
||||
app_groups = {"max"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
workdir = self.launch_context.env.get("AVALON_WORKDIR", "")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,6 @@ class CollectMembers(pyblish.api.InstancePlugin):
|
|||
container = rt.GetNodeByName(instance.data["instance_node"])
|
||||
instance.data["members"] = [
|
||||
member.node for member
|
||||
in container.openPypeData.all_handles
|
||||
in container.modifiers[0].openPypeData.all_handles
|
||||
]
|
||||
self.log.debug("{}".format(instance.data["members"]))
|
||||
|
|
|
|||
|
|
@ -27,20 +27,16 @@ from openpype.settings import get_project_settings
|
|||
from openpype.pipeline import (
|
||||
get_current_project_name,
|
||||
get_current_asset_name,
|
||||
get_current_task_name,
|
||||
discover_loader_plugins,
|
||||
loaders_from_representation,
|
||||
get_representation_path,
|
||||
load_container,
|
||||
registered_host,
|
||||
registered_host
|
||||
)
|
||||
from openpype.lib import NumberDef
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
from openpype.pipeline.create import CreateContext
|
||||
from openpype.pipeline.context_tools import (
|
||||
get_current_asset_name,
|
||||
get_current_project_name,
|
||||
get_current_task_name
|
||||
)
|
||||
from openpype.lib.profiles_filtering import filter_profiles
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,24 @@ from maya import cmds
|
|||
from maya.app.renderSetup.model import renderSetup
|
||||
|
||||
from openpype.lib import BoolDef, Logger
|
||||
from openpype.pipeline import AVALON_CONTAINER_ID, Anatomy, CreatedInstance
|
||||
from openpype.pipeline import Creator as NewCreator
|
||||
from openpype.pipeline import (
|
||||
CreatorError, LegacyCreator, LoaderPlugin, get_representation_path,
|
||||
legacy_io)
|
||||
from openpype.pipeline.load import LoadError
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import (
|
||||
AVALON_CONTAINER_ID,
|
||||
Anatomy,
|
||||
|
||||
CreatedInstance,
|
||||
Creator as NewCreator,
|
||||
AutoCreator,
|
||||
HiddenCreator,
|
||||
|
||||
CreatorError,
|
||||
LegacyCreator,
|
||||
LoaderPlugin,
|
||||
get_representation_path,
|
||||
)
|
||||
from openpype.pipeline.load import LoadError
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline.create import get_subset_name
|
||||
|
||||
from . import lib
|
||||
from .lib import imprint, read
|
||||
|
|
@ -177,10 +188,42 @@ class MayaCreatorBase(object):
|
|||
|
||||
return node_data
|
||||
|
||||
def _default_collect_instances(self):
|
||||
self.cache_subsets(self.collection_shared_data)
|
||||
cached_subsets = self.collection_shared_data["maya_cached_subsets"]
|
||||
for node in cached_subsets.get(self.identifier, []):
|
||||
node_data = self.read_instance_node(node)
|
||||
|
||||
created_instance = CreatedInstance.from_existing(node_data, self)
|
||||
self._add_instance_to_context(created_instance)
|
||||
|
||||
def _default_update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
data = created_inst.data_to_store()
|
||||
node = data.get("instance_node")
|
||||
|
||||
self.imprint_instance_node(node, data)
|
||||
|
||||
def _default_remove_instances(self, instances):
|
||||
"""Remove specified instance from the scene.
|
||||
|
||||
This is only removing `id` parameter so instance is no longer
|
||||
instance, because it might contain valuable data for artist.
|
||||
|
||||
"""
|
||||
for instance in instances:
|
||||
node = instance.data.get("instance_node")
|
||||
if node:
|
||||
cmds.delete(node)
|
||||
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class MayaCreator(NewCreator, MayaCreatorBase):
|
||||
|
||||
settings_name = None
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
|
||||
members = list()
|
||||
|
|
@ -202,34 +245,13 @@ class MayaCreator(NewCreator, MayaCreatorBase):
|
|||
return instance
|
||||
|
||||
def collect_instances(self):
|
||||
self.cache_subsets(self.collection_shared_data)
|
||||
cached_subsets = self.collection_shared_data["maya_cached_subsets"]
|
||||
for node in cached_subsets.get(self.identifier, []):
|
||||
node_data = self.read_instance_node(node)
|
||||
|
||||
created_instance = CreatedInstance.from_existing(node_data, self)
|
||||
self._add_instance_to_context(created_instance)
|
||||
return self._default_collect_instances()
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
data = created_inst.data_to_store()
|
||||
node = data.get("instance_node")
|
||||
|
||||
self.imprint_instance_node(node, data)
|
||||
return self._default_update_instances(update_list)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
"""Remove specified instance from the scene.
|
||||
|
||||
This is only removing `id` parameter so instance is no longer
|
||||
instance, because it might contain valuable data for artist.
|
||||
|
||||
"""
|
||||
for instance in instances:
|
||||
node = instance.data.get("instance_node")
|
||||
if node:
|
||||
cmds.delete(node)
|
||||
|
||||
self._remove_instance_from_context(instance)
|
||||
return self._default_remove_instances(instances)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
return [
|
||||
|
|
@ -238,6 +260,61 @@ class MayaCreator(NewCreator, MayaCreatorBase):
|
|||
default=True)
|
||||
]
|
||||
|
||||
def apply_settings(self, project_settings, system_settings):
|
||||
"""Method called on initialization of plugin to apply settings."""
|
||||
|
||||
settings_name = self.settings_name
|
||||
if settings_name is None:
|
||||
settings_name = self.__class__.__name__
|
||||
|
||||
settings = project_settings["maya"]["create"]
|
||||
settings = settings.get(settings_name)
|
||||
if settings is None:
|
||||
self.log.debug(
|
||||
"No settings found for {}".format(self.__class__.__name__)
|
||||
)
|
||||
return
|
||||
|
||||
for key, value in settings.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class MayaAutoCreator(AutoCreator, MayaCreatorBase):
|
||||
"""Automatically triggered creator for Maya.
|
||||
|
||||
The plugin is not visible in UI, and 'create' method does not expect
|
||||
any arguments.
|
||||
"""
|
||||
|
||||
def collect_instances(self):
|
||||
return self._default_collect_instances()
|
||||
|
||||
def update_instances(self, update_list):
|
||||
return self._default_update_instances(update_list)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
return self._default_remove_instances(instances)
|
||||
|
||||
|
||||
class MayaHiddenCreator(HiddenCreator, MayaCreatorBase):
|
||||
"""Hidden creator for Maya.
|
||||
|
||||
The plugin is not visible in UI, and it does not have strictly defined
|
||||
arguments for 'create' method.
|
||||
"""
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
return MayaCreator.create(self, *args, **kwargs)
|
||||
|
||||
def collect_instances(self):
|
||||
return self._default_collect_instances()
|
||||
|
||||
def update_instances(self, update_list):
|
||||
return self._default_update_instances(update_list)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
return self._default_remove_instances(instances)
|
||||
|
||||
|
||||
def ensure_namespace(namespace):
|
||||
"""Make sure the namespace exists.
|
||||
|
|
@ -328,14 +405,21 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
|
|||
# No existing scene instance node for this layer. Note that
|
||||
# this instance will not have the `instance_node` data yet
|
||||
# until it's been saved/persisted at least once.
|
||||
# TODO: Correctly define the subset name using templates
|
||||
prefix = self.layer_instance_prefix or self.family
|
||||
subset_name = "{}{}".format(prefix, layer.name())
|
||||
project_name = self.create_context.get_current_project_name()
|
||||
|
||||
instance_data = {
|
||||
"asset": legacy_io.Session["AVALON_ASSET"],
|
||||
"task": legacy_io.Session["AVALON_TASK"],
|
||||
"asset": self.create_context.get_current_asset_name(),
|
||||
"task": self.create_context.get_current_task_name(),
|
||||
"variant": layer.name(),
|
||||
}
|
||||
asset_doc = get_asset_by_name(project_name,
|
||||
instance_data["asset"])
|
||||
subset_name = self.get_subset_name(
|
||||
layer.name(),
|
||||
instance_data["task"],
|
||||
asset_doc,
|
||||
project_name)
|
||||
|
||||
instance = CreatedInstance(
|
||||
family=self.family,
|
||||
subset_name=subset_name,
|
||||
|
|
@ -362,7 +446,7 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
|
|||
|
||||
creator_identifier = cmds.getAttr(node + ".creator_identifier")
|
||||
if creator_identifier == self.identifier:
|
||||
self.log.info(f"Found node: {node}")
|
||||
self.log.info("Found node: {}".format(node))
|
||||
return node
|
||||
|
||||
def _create_layer_instance_node(self, layer):
|
||||
|
|
@ -442,10 +526,75 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
|
|||
if node and cmds.objExists(node):
|
||||
cmds.delete(node)
|
||||
|
||||
def get_subset_name(
|
||||
self,
|
||||
variant,
|
||||
task_name,
|
||||
asset_doc,
|
||||
project_name,
|
||||
host_name=None,
|
||||
instance=None
|
||||
):
|
||||
# creator.family != 'render' as expected
|
||||
return get_subset_name(self.layer_instance_prefix,
|
||||
variant,
|
||||
task_name,
|
||||
asset_doc,
|
||||
project_name)
|
||||
|
||||
|
||||
class Loader(LoaderPlugin):
|
||||
hosts = ["maya"]
|
||||
|
||||
def get_custom_namespace_and_group(self, context, options, loader_key):
|
||||
"""Queries Settings to get custom template for namespace and group.
|
||||
|
||||
Group template might be empty >> this forces to not wrap imported items
|
||||
into separate group.
|
||||
|
||||
Args:
|
||||
context (dict)
|
||||
options (dict): artist modifiable options from dialog
|
||||
loader_key (str): key to get separate configuration from Settings
|
||||
('reference_loader'|'import_loader')
|
||||
"""
|
||||
options["attach_to_root"] = True
|
||||
|
||||
asset = context['asset']
|
||||
subset = context['subset']
|
||||
settings = get_project_settings(context['project']['name'])
|
||||
custom_naming = settings['maya']['load'][loader_key]
|
||||
|
||||
if not custom_naming['namespace']:
|
||||
raise LoadError("No namespace specified in "
|
||||
"Maya ReferenceLoader settings")
|
||||
elif not custom_naming['group_name']:
|
||||
self.log.debug("No custom group_name, no group will be created.")
|
||||
options["attach_to_root"] = False
|
||||
|
||||
formatting_data = {
|
||||
"asset_name": asset['name'],
|
||||
"asset_type": asset['type'],
|
||||
"folder": {
|
||||
"name": asset["name"],
|
||||
},
|
||||
"subset": subset['name'],
|
||||
"family": (
|
||||
subset['data'].get('family') or
|
||||
subset['data']['families'][0]
|
||||
)
|
||||
}
|
||||
|
||||
custom_namespace = custom_naming['namespace'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
custom_group_name = custom_naming['group_name'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
return custom_group_name, custom_namespace, options
|
||||
|
||||
|
||||
class ReferenceLoader(Loader):
|
||||
"""A basic ReferenceLoader for Maya
|
||||
|
|
@ -488,39 +637,13 @@ class ReferenceLoader(Loader):
|
|||
path = self.filepath_from_context(context)
|
||||
assert os.path.exists(path), "%s does not exist." % path
|
||||
|
||||
asset = context['asset']
|
||||
subset = context['subset']
|
||||
settings = get_project_settings(context['project']['name'])
|
||||
custom_naming = settings['maya']['load']['reference_loader']
|
||||
loaded_containers = []
|
||||
|
||||
if not custom_naming['namespace']:
|
||||
raise LoadError("No namespace specified in "
|
||||
"Maya ReferenceLoader settings")
|
||||
elif not custom_naming['group_name']:
|
||||
raise LoadError("No group name specified in "
|
||||
"Maya ReferenceLoader settings")
|
||||
|
||||
formatting_data = {
|
||||
"asset_name": asset['name'],
|
||||
"asset_type": asset['type'],
|
||||
"subset": subset['name'],
|
||||
"family": (
|
||||
subset['data'].get('family') or
|
||||
subset['data']['families'][0]
|
||||
)
|
||||
}
|
||||
|
||||
custom_namespace = custom_naming['namespace'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
custom_group_name = custom_naming['group_name'].format(
|
||||
**formatting_data
|
||||
)
|
||||
custom_group_name, custom_namespace, options = \
|
||||
self.get_custom_namespace_and_group(context, options,
|
||||
"reference_loader")
|
||||
|
||||
count = options.get("count") or 1
|
||||
|
||||
loaded_containers = []
|
||||
for c in range(0, count):
|
||||
namespace = lib.get_custom_namespace(custom_namespace)
|
||||
group_name = "{}:{}".format(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class MayaPreAutoLoadPlugins(PreLaunchHook):
|
||||
|
|
@ -6,7 +6,8 @@ class MayaPreAutoLoadPlugins(PreLaunchHook):
|
|||
|
||||
# Before AddLastWorkfileToLaunchArgs
|
||||
order = 9
|
||||
app_groups = ["maya"]
|
||||
app_groups = {"maya"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from openpype.hosts.maya.lib import create_workspace_mel
|
||||
|
||||
|
||||
|
|
@ -7,7 +7,8 @@ class PreCopyMel(PreLaunchHook):
|
|||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = ["maya"]
|
||||
app_groups = {"maya"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
project_doc = self.data["project_doc"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from openpype.lib import PreLaunchHook
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class MayaPreOpenWorkfilePostInitialization(PreLaunchHook):
|
||||
|
|
@ -6,7 +6,8 @@ class MayaPreOpenWorkfilePostInitialization(PreLaunchHook):
|
|||
|
||||
# Before AddLastWorkfileToLaunchArgs.
|
||||
order = 9
|
||||
app_groups = ["maya"]
|
||||
app_groups = {"maya"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin
|
|||
from openpype.hosts.maya.api import plugin
|
||||
from openpype.hosts.maya.api.lib import read
|
||||
|
||||
from openpype.client import get_asset_by_name
|
||||
|
||||
from maya import cmds
|
||||
from maya.app.renderSetup.model import renderSetup
|
||||
|
||||
|
|
@ -51,7 +53,7 @@ class MayaLegacyConvertor(SubsetConvertorPlugin,
|
|||
# From all current new style manual creators find the mapping
|
||||
# from family to identifier
|
||||
family_to_id = {}
|
||||
for identifier, creator in self.create_context.manual_creators.items():
|
||||
for identifier, creator in self.create_context.creators.items():
|
||||
family = getattr(creator, "family", None)
|
||||
if not family:
|
||||
continue
|
||||
|
|
@ -70,7 +72,6 @@ class MayaLegacyConvertor(SubsetConvertorPlugin,
|
|||
# logic was thus to be live to the current task to begin with.
|
||||
data = dict()
|
||||
data["task"] = self.create_context.get_current_task_name()
|
||||
|
||||
for family, instance_nodes in legacy.items():
|
||||
if family not in family_to_id:
|
||||
self.log.warning(
|
||||
|
|
@ -81,7 +82,7 @@ class MayaLegacyConvertor(SubsetConvertorPlugin,
|
|||
continue
|
||||
|
||||
creator_id = family_to_id[family]
|
||||
creator = self.create_context.manual_creators[creator_id]
|
||||
creator = self.create_context.creators[creator_id]
|
||||
data["creator_identifier"] = creator_id
|
||||
|
||||
if isinstance(creator, plugin.RenderlayerCreator):
|
||||
|
|
@ -136,6 +137,18 @@ class MayaLegacyConvertor(SubsetConvertorPlugin,
|
|||
# "rendering" family being converted to "renderlayer" family)
|
||||
original_data["family"] = creator.family
|
||||
|
||||
# recreate subset name as without it would be
|
||||
# `renderingMain` vs correct `renderMain`
|
||||
project_name = self.create_context.get_current_project_name()
|
||||
asset_doc = get_asset_by_name(project_name,
|
||||
original_data["asset"])
|
||||
subset_name = creator.get_subset_name(
|
||||
original_data["variant"],
|
||||
data["task"],
|
||||
asset_doc,
|
||||
project_name)
|
||||
original_data["subset"] = subset_name
|
||||
|
||||
# Convert to creator attributes when relevant
|
||||
creator_attributes = {}
|
||||
for key in list(original_data.keys()):
|
||||
|
|
|
|||
|
|
@ -8,15 +8,13 @@ from openpype.lib import (
|
|||
)
|
||||
|
||||
|
||||
class CreateAnimation(plugin.MayaCreator):
|
||||
"""Animation output for character rigs"""
|
||||
|
||||
# We hide the animation creator from the UI since the creation of it
|
||||
# is automated upon loading a rig. There's an inventory action to recreate
|
||||
# it for loaded rigs if by chance someone deleted the animation instance.
|
||||
# Note: This setting is actually applied from project settings
|
||||
enabled = False
|
||||
class CreateAnimation(plugin.MayaHiddenCreator):
|
||||
"""Animation output for character rigs
|
||||
|
||||
We hide the animation creator from the UI since the creation of it is
|
||||
automated upon loading a rig. There's an inventory action to recreate it
|
||||
for loaded rigs if by chance someone deleted the animation instance.
|
||||
"""
|
||||
identifier = "io.openpype.creators.maya.animation"
|
||||
name = "animationDefault"
|
||||
label = "Animation"
|
||||
|
|
@ -28,9 +26,6 @@ class CreateAnimation(plugin.MayaCreator):
|
|||
include_parent_hierarchy = False
|
||||
include_user_defined_attributes = False
|
||||
|
||||
# TODO: Would be great if we could visually hide this from the creator
|
||||
# by default but do allow to generate it through code.
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
|
||||
defs = lib.collect_animation_defs()
|
||||
|
|
@ -85,3 +80,12 @@ class CreateAnimation(plugin.MayaCreator):
|
|||
"""
|
||||
|
||||
return defs
|
||||
|
||||
def apply_settings(self, project_settings, system_settings):
|
||||
super(CreateAnimation, self).apply_settings(
|
||||
project_settings, system_settings
|
||||
)
|
||||
# Hardcoding creator to be enabled due to existing settings would
|
||||
# disable the creator causing the creator plugin to not be
|
||||
# discoverable.
|
||||
self.enabled = True
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class CreateArnoldSceneSource(plugin.MayaCreator):
|
|||
label = "Arnold Scene Source"
|
||||
family = "ass"
|
||||
icon = "cube"
|
||||
settings_name = "CreateAss"
|
||||
|
||||
expandProcedurals = False
|
||||
motionBlur = True
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class CreateModel(plugin.MayaCreator):
|
|||
label = "Model"
|
||||
family = "model"
|
||||
icon = "cube"
|
||||
defaults = ["Main", "Proxy", "_MD", "_HD", "_LD"]
|
||||
default_variants = ["Main", "Proxy", "_MD", "_HD", "_LD"]
|
||||
|
||||
write_color_sets = False
|
||||
write_face_sets = False
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ class CreateRig(plugin.MayaCreator):
|
|||
instance_node = instance.get("instance_node")
|
||||
|
||||
self.log.info("Creating Rig instance set up ...")
|
||||
controls = cmds.sets(name="controls_SET", empty=True)
|
||||
pointcache = cmds.sets(name="out_SET", empty=True)
|
||||
controls = cmds.sets(name=subset_name + "_controls_SET", empty=True)
|
||||
pointcache = cmds.sets(name=subset_name + "_out_SET", empty=True)
|
||||
cmds.sets([controls, pointcache], forceElement=instance_node)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class CreateSetDress(plugin.MayaCreator):
|
|||
label = "Set Dress"
|
||||
family = "setdress"
|
||||
icon = "cubes"
|
||||
defaults = ["Main", "Anim"]
|
||||
default_variants = ["Main", "Anim"]
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
suffix="_abc"
|
||||
)
|
||||
|
||||
attach_to_root = options.get("attach_to_root", True)
|
||||
group_name = options["group_name"]
|
||||
|
||||
# no group shall be created
|
||||
if not attach_to_root:
|
||||
group_name = namespace
|
||||
|
||||
# hero_001 (abc)
|
||||
# asset_counter{optional}
|
||||
path = self.filepath_from_context(context)
|
||||
|
|
@ -41,8 +48,8 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
nodes = cmds.file(file_url,
|
||||
namespace=namespace,
|
||||
sharedReferenceFile=False,
|
||||
groupReference=True,
|
||||
groupName=options['group_name'],
|
||||
groupReference=attach_to_root,
|
||||
groupName=group_name,
|
||||
reference=True,
|
||||
returnNewNodes=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ import qargparse
|
|||
from openpype.pipeline import load
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
maintained_selection,
|
||||
unique_namespace
|
||||
get_custom_namespace
|
||||
)
|
||||
import openpype.hosts.maya.api.plugin
|
||||
|
||||
|
||||
class SetFrameRangeLoader(load.LoaderPlugin):
|
||||
|
|
@ -83,7 +84,7 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
|
|||
animationEndTime=end)
|
||||
|
||||
|
||||
class ImportMayaLoader(load.LoaderPlugin):
|
||||
class ImportMayaLoader(openpype.hosts.maya.api.plugin.Loader):
|
||||
"""Import action for Maya (unmanaged)
|
||||
|
||||
Warning:
|
||||
|
|
@ -130,13 +131,14 @@ class ImportMayaLoader(load.LoaderPlugin):
|
|||
if choice is False:
|
||||
return
|
||||
|
||||
asset = context['asset']
|
||||
custom_group_name, custom_namespace, options = \
|
||||
self.get_custom_namespace_and_group(context, data,
|
||||
"import_loader")
|
||||
|
||||
namespace = namespace or unique_namespace(
|
||||
asset["name"] + "_",
|
||||
prefix="_" if asset["name"][0].isdigit() else "",
|
||||
suffix="_",
|
||||
)
|
||||
namespace = get_custom_namespace(custom_namespace)
|
||||
|
||||
if not options.get("attach_to_root", True):
|
||||
custom_group_name = namespace
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
with maintained_selection():
|
||||
|
|
@ -145,8 +147,9 @@ class ImportMayaLoader(load.LoaderPlugin):
|
|||
preserveReferences=True,
|
||||
namespace=namespace,
|
||||
returnNewNodes=True,
|
||||
groupReference=True,
|
||||
groupName="{}:{}".format(namespace, name))
|
||||
groupReference=options.get("attach_to_root",
|
||||
True),
|
||||
groupName=custom_group_name)
|
||||
|
||||
if data.get("clean_import", False):
|
||||
remove_attributes = ["cbId"]
|
||||
|
|
|
|||
|
|
@ -123,6 +123,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
attach_to_root = options.get("attach_to_root", True)
|
||||
group_name = options["group_name"]
|
||||
|
||||
# no group shall be created
|
||||
if not attach_to_root:
|
||||
group_name = namespace
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
with maintained_selection():
|
||||
cmds.loadPlugin("AbcImport.mll", quiet=True)
|
||||
|
|
@ -148,11 +152,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
if current_namespace != ":":
|
||||
group_name = current_namespace + ":" + group_name
|
||||
|
||||
group_name = "|" + group_name
|
||||
|
||||
self[:] = new_nodes
|
||||
|
||||
if attach_to_root:
|
||||
group_name = "|" + group_name
|
||||
roots = cmds.listRelatives(group_name,
|
||||
children=True,
|
||||
fullPath=True) or []
|
||||
|
|
@ -205,6 +208,11 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
self._post_process_rig(name, namespace, context, options)
|
||||
else:
|
||||
if "translate" in options:
|
||||
if not attach_to_root and new_nodes:
|
||||
root_nodes = cmds.ls(new_nodes, assemblies=True,
|
||||
long=True)
|
||||
# we assume only a single root is ever loaded
|
||||
group_name = root_nodes[0]
|
||||
cmds.setAttr("{}.translate".format(group_name),
|
||||
*options["translate"])
|
||||
return new_nodes
|
||||
|
|
|
|||
|
|
@ -19,8 +19,15 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
def process_reference(
|
||||
self, context, name=None, namespace=None, options=None
|
||||
):
|
||||
group_name = options['group_name']
|
||||
path = self.filepath_from_context(context)
|
||||
|
||||
attach_to_root = options.get("attach_to_root", True)
|
||||
group_name = options["group_name"]
|
||||
|
||||
# no group shall be created
|
||||
if not attach_to_root:
|
||||
group_name = namespace
|
||||
|
||||
with lib.maintained_selection():
|
||||
file_url = self.prepare_root_value(
|
||||
path, context["project"]["name"]
|
||||
|
|
@ -30,7 +37,7 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
namespace=namespace,
|
||||
reference=True,
|
||||
returnNewNodes=True,
|
||||
groupReference=True,
|
||||
groupReference=attach_to_root,
|
||||
groupName=group_name
|
||||
)
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue