Merge branch 'develop' into feature/tvpaint_load_workfile

This commit is contained in:
Toke Stuart Jepsen 2021-09-25 11:39:41 +01:00
commit c1d48ae990
247 changed files with 12001 additions and 1714 deletions

View file

@ -20,12 +20,12 @@ jobs:
python-version: 3.7
- name: Install Python requirements
run: pip install gitpython semver
run: pip install gitpython semver PyGithub
- name: 🔎 Determine next version type
id: version_type
run: |
TYPE=$(python ./tools/ci_tools.py --bump)
TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.GITHUB_TOKEN }})
echo ::set-output name=type::$TYPE
@ -43,11 +43,7 @@ jobs:
uses: heinrichreimer/github-changelog-generator-action@v2.2
with:
token: ${{ secrets.ADMIN_TOKEN }}
breakingLabel: '**💥 Breaking**'
enhancementLabel: '**🚀 Enhancements**'
bugsLabel: '**🐛 Bug fixes**'
deprecatedLabel: '**⚠️ Deprecations**'
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"### 🆕 New features","labels":["feature"]},}'
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}'
issues: false
issuesWoLabels: false
sinceTag: "3.0.0"
@ -80,6 +76,7 @@ jobs:
git add .
git commit -m "[Automated] Bump version"
tag_name="CI/${{ steps.version.outputs.next_tag }}"
echo $tag_name
git tag -a $tag_name -m "nightly build"
- name: Push to protected main branch

View file

@ -21,7 +21,7 @@ jobs:
with:
python-version: 3.7
- name: Install Python requirements
run: pip install gitpython semver
run: pip install gitpython semver PyGithub
- name: 💉 Inject new version into files
id: version
@ -39,11 +39,7 @@ jobs:
uses: heinrichreimer/github-changelog-generator-action@v2.2
with:
token: ${{ secrets.ADMIN_TOKEN }}
breakingLabel: '**💥 Breaking**'
enhancementLabel: '**🚀 Enhancements**'
bugsLabel: '**🐛 Bug fixes**'
deprecatedLabel: '**⚠️ Deprecations**'
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
addSections: '{"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]},"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]}}'
issues: false
issuesWoLabels: false
sinceTag: "3.0.0"
@ -85,11 +81,7 @@ jobs:
uses: heinrichreimer/github-changelog-generator-action@v2.2
with:
token: ${{ secrets.ADMIN_TOKEN }}
breakingLabel: '**💥 Breaking**'
enhancementLabel: '**🚀 Enhancements**'
bugsLabel: '**🐛 Bug fixes**'
deprecatedLabel: '**⚠️ Deprecations**'
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}'
issues: false
issuesWoLabels: false
sinceTag: ${{ steps.version.outputs.last_release }}

2
.gitmodules vendored
View file

@ -9,4 +9,4 @@
url = https://github.com/arrow-py/arrow.git
[submodule "openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api"]
path = openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api
url = https://bitbucket.org/ftrack/ftrack-python-api.git
url = https://bitbucket.org/ftrack/ftrack-python-api.git

View file

@ -1,91 +1,150 @@
# Changelog
## [3.4.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.5.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD)
**🆕 New features**
- Maya: Validate setdress top group [\#2068](https://github.com/pypeclub/OpenPype/pull/2068)
- Maya: Enable publishing render attrib sets \(e.g. V-Ray Displacement\) with model [\#1955](https://github.com/pypeclub/OpenPype/pull/1955)
**🚀 Enhancements**
- Project manager: Filter first item after selection of project [\#2069](https://github.com/pypeclub/OpenPype/pull/2069)
- Tools: add support for pyenv on windows [\#2051](https://github.com/pypeclub/OpenPype/pull/2051)
**🐛 Bug fixes**
- Fix Sync Queue when project disabled [\#2063](https://github.com/pypeclub/OpenPype/pull/2063)
- TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946)
## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.1-nightly.1...3.4.1)
**🆕 New features**
- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008)
**🚀 Enhancements**
- General: Startup validations [\#2054](https://github.com/pypeclub/OpenPype/pull/2054)
- Nuke: proxy mode validator [\#2052](https://github.com/pypeclub/OpenPype/pull/2052)
- Ftrack: Removed ftrack interface [\#2049](https://github.com/pypeclub/OpenPype/pull/2049)
- Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044)
- Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043)
- Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042)
- Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039)
- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038)
- Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030)
- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028)
- TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024)
- Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018)
**🐛 Bug fixes**
- Timers manger: Typo fix [\#2058](https://github.com/pypeclub/OpenPype/pull/2058)
- Hiero: Editorial fixes [\#2057](https://github.com/pypeclub/OpenPype/pull/2057)
- Differentiate jpg sequences from thumbnail [\#2056](https://github.com/pypeclub/OpenPype/pull/2056)
- FFmpeg: Split command to list does not work [\#2046](https://github.com/pypeclub/OpenPype/pull/2046)
- Removed shell flag in subprocess call [\#2045](https://github.com/pypeclub/OpenPype/pull/2045)
**Merged pull requests:**
- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972)
- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967)
- Bump prismjs from 1.24.0 to 1.25.0 in /website [\#2050](https://github.com/pypeclub/OpenPype/pull/2050)
## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0)
### 📖 Documentation
- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014)
- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952)
**🆕 New features**
- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003)
**🚀 Enhancements**
- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041)
- General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036)
- Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022)
- Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019)
- General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017)
- Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015)
- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009)
- Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001)
- Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996)
- Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987)
- Global: Example addons [\#1986](https://github.com/pypeclub/OpenPype/pull/1986)
- Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982)
- Settings UI: Number sliders [\#1978](https://github.com/pypeclub/OpenPype/pull/1978)
- Workfiles: Support more workfile templates [\#1966](https://github.com/pypeclub/OpenPype/pull/1966)
- Launcher: Fix crashes on action click [\#1964](https://github.com/pypeclub/OpenPype/pull/1964)
- Settings: Minor fixes in UI and missing default values [\#1963](https://github.com/pypeclub/OpenPype/pull/1963)
- Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962)
- Resolve path when adding to zip [\#1960](https://github.com/pypeclub/OpenPype/pull/1960)
- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958)
- Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959)
- CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954)
- Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949)
- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948)
- Maya: Add Xgen family support [\#1947](https://github.com/pypeclub/OpenPype/pull/1947)
- Add face sets to exported alembics [\#1942](https://github.com/pypeclub/OpenPype/pull/1942)
- Bump path-parse from 1.0.6 to 1.0.7 in /website [\#1933](https://github.com/pypeclub/OpenPype/pull/1933)
- \#1894 - adds host to template\_name\_profiles for filtering [\#1915](https://github.com/pypeclub/OpenPype/pull/1915)
- Environments: Tool environments in alphabetical order [\#1910](https://github.com/pypeclub/OpenPype/pull/1910)
- Disregard publishing time. [\#1888](https://github.com/pypeclub/OpenPype/pull/1888)
- Feature/webpublisher backend [\#1876](https://github.com/pypeclub/OpenPype/pull/1876)
- Dynamic modules [\#1872](https://github.com/pypeclub/OpenPype/pull/1872)
- Houdini: add Camera, Point Cache, Composite, Redshift ROP and VDB Cache support [\#1821](https://github.com/pypeclub/OpenPype/pull/1821)
**🐛 Bug fixes**
- Workfiles tool: Task selection [\#2040](https://github.com/pypeclub/OpenPype/pull/2040)
- Ftrack: Delete old versions missing settings key [\#2037](https://github.com/pypeclub/OpenPype/pull/2037)
- Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034)
- Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033)
- FFmpeg: Subprocess arguments as list [\#2032](https://github.com/pypeclub/OpenPype/pull/2032)
- General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016)
- Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006)
- Nuke thumbnails generated from middle of the sequence [\#1992](https://github.com/pypeclub/OpenPype/pull/1992)
- Nuke: last version from path gets correct version [\#1990](https://github.com/pypeclub/OpenPype/pull/1990)
- nuke, resolve, hiero: precollector order lest then 0.5 [\#1984](https://github.com/pypeclub/OpenPype/pull/1984)
- Last workfile with multiple work templates [\#1981](https://github.com/pypeclub/OpenPype/pull/1981)
- Collectors order [\#1977](https://github.com/pypeclub/OpenPype/pull/1977)
- Stop timer was within validator order range. [\#1975](https://github.com/pypeclub/OpenPype/pull/1975)
- Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974)
- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972)
- Deadline: Houdini plugins in different hierarchy [\#1970](https://github.com/pypeclub/OpenPype/pull/1970)
- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967)
- Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961)
## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1)
**Merged pull requests:**
**🚀 Enhancements**
- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939)
**🐛 Bug fixes**
- TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946)
- Maya: Menu actions fix [\#1945](https://github.com/pypeclub/OpenPype/pull/1945)
- standalone: editorial shared object problem [\#1941](https://github.com/pypeclub/OpenPype/pull/1941)
- Bugfix nuke deadline app name [\#1928](https://github.com/pypeclub/OpenPype/pull/1928)
## [3.3.0](https://github.com/pypeclub/OpenPype/tree/3.3.0) (2021-08-17)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.0-nightly.11...3.3.0)
**Merged pull requests:**
**🆕 New features**
- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932)
**🚀 Enhancements**
- Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940)
- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937)
**🐛 Bug fixes**
- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935)
- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932)
- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930)
- Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929)
- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927)
- Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926)
- Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925)
- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923)
- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922)
- Settings: Default values for enum [\#1920](https://github.com/pypeclub/OpenPype/pull/1920)
- Settings UI: Modifiable dict view enhance [\#1919](https://github.com/pypeclub/OpenPype/pull/1919)
- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917)
- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916)
- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914)
- submodules: avalon-core update [\#1911](https://github.com/pypeclub/OpenPype/pull/1911)
- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906)
- Add support for multiple Deadline ☠️➖ servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905)
- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904)
- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903)
- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902)
- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901)
- Ftrack: Where I run action enhancement [\#1900](https://github.com/pypeclub/OpenPype/pull/1900)
- Ftrack: Private project server actions [\#1899](https://github.com/pypeclub/OpenPype/pull/1899)
- Support nested studio plugins paths. [\#1898](https://github.com/pypeclub/OpenPype/pull/1898)
- Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893)
- Settings: global validators with options [\#1892](https://github.com/pypeclub/OpenPype/pull/1892)
- Settings: Conditional dict enum positioning [\#1891](https://github.com/pypeclub/OpenPype/pull/1891)
- global: integrate name missing default template [\#1890](https://github.com/pypeclub/OpenPype/pull/1890)
- publisher: editorial plugins fixes [\#1889](https://github.com/pypeclub/OpenPype/pull/1889)
- Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886)
- TVPaint: Increment workfile [\#1885](https://github.com/pypeclub/OpenPype/pull/1885)
- Allow Multiple Notes to run on tasks. [\#1882](https://github.com/pypeclub/OpenPype/pull/1882)
- Normalize path returned from Workfiles. [\#1880](https://github.com/pypeclub/OpenPype/pull/1880)
- Prepare for pyside2 [\#1869](https://github.com/pypeclub/OpenPype/pull/1869)
- Filter hosts in settings host-enum [\#1868](https://github.com/pypeclub/OpenPype/pull/1868)
- Local actions with process identifier [\#1867](https://github.com/pypeclub/OpenPype/pull/1867)
- Workfile tool start at host launch support [\#1865](https://github.com/pypeclub/OpenPype/pull/1865)
- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space 🚀 [\#1863](https://github.com/pypeclub/OpenPype/pull/1863)
- Workfiles tool event arguments fix [\#1862](https://github.com/pypeclub/OpenPype/pull/1862)
- Maya: support for configurable `dirmap` 🗺️ [\#1859](https://github.com/pypeclub/OpenPype/pull/1859)
- Maya: don't add reference members as connections to the container set 📦 [\#1855](https://github.com/pypeclub/OpenPype/pull/1855)
- Settings list can use template or schema as object type [\#1815](https://github.com/pypeclub/OpenPype/pull/1815)
**Merged pull requests:**
- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937)
## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13)

View file

@ -12,6 +12,9 @@ from .version import __version__ as version
def open_dialog():
"""Show Igniter dialog."""
if os.getenv("OPENPYPE_HEADLESS_MODE"):
print("!!! Can't open dialog in headless mode. Exiting.")
sys.exit(1)
from Qt import QtWidgets, QtCore
from .install_dialog import InstallDialog
@ -28,8 +31,31 @@ def open_dialog():
return d.result()
def open_update_window(openpype_version):
"""Open update window."""
if os.getenv("OPENPYPE_HEADLESS_MODE"):
print("!!! Can't open dialog in headless mode. Exiting.")
sys.exit(1)
from Qt import QtWidgets, QtCore
from .update_window import UpdateWindow
scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None)
if scale_attr is not None:
QtWidgets.QApplication.setAttribute(scale_attr)
app = QtWidgets.QApplication(sys.argv)
d = UpdateWindow(version=openpype_version)
d.open()
app.exec_()
version_path = d.get_version_path()
return version_path
__all__ = [
"BootstrapRepos",
"open_dialog",
"open_update_window",
"version"
]

View file

@ -9,6 +9,7 @@ import sys
import tempfile
from pathlib import Path
from typing import Union, Callable, List, Tuple
import hashlib
from zipfile import ZipFile, BadZipFile
@ -28,6 +29,25 @@ LOG_WARNING = 1
LOG_ERROR = 3
def sha256sum(filename):
"""Calculate sha256 for content of the file.
Args:
filename (str): Path to file.
Returns:
str: hex encoded sha256
"""
h = hashlib.sha256()
b = bytearray(128 * 1024)
mv = memoryview(b)
with open(filename, 'rb', buffering=0) as f:
for n in iter(lambda: f.readinto(mv), 0):
h.update(mv[:n])
return h.hexdigest()
class OpenPypeVersion(semver.VersionInfo):
"""Class for storing information about OpenPype version.
@ -261,7 +281,8 @@ class BootstrapRepos:
self.live_repo_dir = Path(Path(__file__).parent / ".." / "repos")
@staticmethod
def get_version_path_from_list(version: str, version_list: list) -> Path:
def get_version_path_from_list(
version: str, version_list: list) -> Union[Path, None]:
"""Get path for specific version in list of OpenPype versions.
Args:
@ -275,6 +296,7 @@ class BootstrapRepos:
for v in version_list:
if str(v) == version:
return v.path
return None
@staticmethod
def get_local_live_version() -> str:
@ -487,6 +509,7 @@ class BootstrapRepos:
openpype_root = openpype_path.resolve()
# generate list of filtered paths
dir_filter = [openpype_root / f for f in self.openpype_filter]
checksums = []
file: Path
for file in openpype_list:
@ -508,12 +531,119 @@ class BootstrapRepos:
processed_path = file
self._print(f"- processing {processed_path}")
zip_file.write(file, file.resolve().relative_to(openpype_root))
checksums.append(
(
sha256sum(file.as_posix()),
file.resolve().relative_to(openpype_root)
)
)
zip_file.write(
file, file.resolve().relative_to(openpype_root))
checksums_str = ""
for c in checksums:
checksums_str += "{}:{}\n".format(c[0], c[1])
zip_file.writestr("checksums", checksums_str)
# test if zip is ok
zip_file.testzip()
self._progress_callback(100)
def validate_openpype_version(self, path: Path) -> tuple:
"""Validate version directory or zip file.
This will load `checksums` file if present, calculate checksums
of existing files in given path and compare. It will also compare
lists of files together for missing files.
Args:
path (Path): Path to OpenPype version to validate.
Returns:
tuple(bool, str): with version validity as first item
and string with reason as second.
"""
if not path.exists():
return False, "Path doesn't exist"
if path.is_file():
return self._validate_zip(path)
return self._validate_dir(path)
@staticmethod
def _validate_zip(path: Path) -> tuple:
"""Validate content of zip file."""
with ZipFile(path, "r") as zip_file:
# read checksums
try:
checksums_data = str(zip_file.read("checksums"))
except IOError:
# FIXME: This should be set to False sometimes in the future
return True, "Cannot read checksums for archive."
# split it to the list of tuples
checksums = [
tuple(line.split(":"))
for line in checksums_data.split("\n") if line
]
# calculate and compare checksums in the zip file
for file in checksums:
h = hashlib.sha256()
try:
h.update(zip_file.read(file[1]))
except FileNotFoundError:
return False, f"Missing file [ {file[1]} ]"
if h.hexdigest() != file[0]:
return False, f"Invalid checksum on {file[1]}"
# get list of files in zip minus `checksums` file itself
# and turn in to set to compare against list of files
# from checksum file. If difference exists, something is
# wrong
files_in_zip = zip_file.namelist()
files_in_zip.remove("checksums")
files_in_zip = set(files_in_zip)
files_in_checksum = set([file[1] for file in checksums])
diff = files_in_zip.difference(files_in_checksum)
if diff:
return False, f"Missing files {diff}"
return True, "All ok"
@staticmethod
def _validate_dir(path: Path) -> tuple:
checksums_file = Path(path / "checksums")
if not checksums_file.exists():
# FIXME: This should be set to False sometimes in the future
return True, "Cannot read checksums for archive."
checksums_data = checksums_file.read_text()
checksums = [
tuple(line.split(":"))
for line in checksums_data.split("\n") if line
]
files_in_dir = [
file.relative_to(path).as_posix()
for file in path.iterdir() if file.is_file()
]
files_in_dir.remove("checksums")
files_in_dir = set(files_in_dir)
files_in_checksum = set([file[1] for file in checksums])
for file in checksums:
try:
current = sha256sum((path / file[1]).as_posix())
except FileNotFoundError:
return False, f"Missing file [ {file[1]} ]"
if file[0] != current:
return False, f"Invalid checksum on {file[1]}"
diff = files_in_dir.difference(files_in_checksum)
if diff:
return False, f"Missing files {diff}"
return True, "All ok"
@staticmethod
def add_paths_from_archive(archive: Path) -> None:
"""Add first-level directory and 'repos' as paths to :mod:`sys.path`.
@ -837,6 +967,7 @@ class BootstrapRepos:
# test if destination directory already exist, if so lets delete it.
if destination.exists() and force:
self._print("removing existing directory")
try:
shutil.rmtree(destination)
except OSError as e:
@ -846,6 +977,7 @@ class BootstrapRepos:
raise OpenPypeVersionIOError(
f"cannot remove existing {destination}") from e
elif destination.exists() and not force:
self._print("destination directory already exists")
raise OpenPypeVersionExists(f"{destination} already exist.")
else:
# create destination parent directories even if they don't exist.
@ -855,6 +987,7 @@ class BootstrapRepos:
if openpype_version.path.is_dir():
# create zip inside temporary directory.
self._print("Creating zip from directory ...")
self._progress_callback(0)
with tempfile.TemporaryDirectory() as temp_dir:
temp_zip = \
Path(temp_dir) / f"openpype-v{openpype_version}.zip"
@ -880,13 +1013,16 @@ class BootstrapRepos:
raise OpenPypeVersionInvalid("Invalid file format")
if not self.is_inside_user_data(openpype_version.path):
self._progress_callback(35)
openpype_version.path = self._copy_zip(
openpype_version.path, destination)
# extract zip there
self._print("extracting zip to destination ...")
with ZipFile(openpype_version.path, "r") as zip_ref:
self._progress_callback(75)
zip_ref.extractall(destination)
self._progress_callback(100)
return destination

View file

@ -14,21 +14,13 @@ from .tools import (
validate_mongo_connection,
get_openpype_path_from_db
)
from .nice_progress_bar import NiceProgressBar
from .user_settings import OpenPypeSecureRegistry
from .tools import load_stylesheet
from .version import __version__
def load_stylesheet():
stylesheet_path = os.path.join(
os.path.dirname(__file__),
"stylesheet.css"
)
with open(stylesheet_path, "r") as file_stream:
stylesheet = file_stream.read()
return stylesheet
class ButtonWithOptions(QtWidgets.QFrame):
option_clicked = QtCore.Signal(str)
@ -91,25 +83,6 @@ class ButtonWithOptions(QtWidgets.QFrame):
self.option_clicked.emit(self._default_value)
class NiceProgressBar(QtWidgets.QProgressBar):
def __init__(self, parent=None):
super(NiceProgressBar, self).__init__(parent)
self._real_value = 0
def setValue(self, value):
self._real_value = value
if value != 0 and value < 11:
value = 11
super(NiceProgressBar, self).setValue(value)
def value(self):
return self._real_value
def text(self):
return "{} %".format(self._real_value)
class ConsoleWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ConsoleWidget, self).__init__(parent)

View file

@ -0,0 +1,20 @@
from Qt import QtCore, QtGui, QtWidgets # noqa
class NiceProgressBar(QtWidgets.QProgressBar):
def __init__(self, parent=None):
super(NiceProgressBar, self).__init__(parent)
self._real_value = 0
def setValue(self, value):
self._real_value = value
if value != 0 and value < 11:
value = 11
super(NiceProgressBar, self).setValue(value)
def value(self):
return self._real_value
def text(self):
return "{} %".format(self._real_value)

View file

@ -248,3 +248,15 @@ def get_openpype_path_from_db(url: str) -> Union[str, None]:
if os.path.exists(path):
return path
return None
def load_stylesheet() -> str:
"""Load css style sheet.
Returns:
str: content of the stylesheet
"""
stylesheet_path = Path(__file__).parent.resolve() / "stylesheet.css"
return stylesheet_path.read_text()

61
igniter/update_thread.py Normal file
View file

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
"""Working thread for update."""
from Qt.QtCore import QThread, Signal, QObject # noqa
from .bootstrap_repos import (
BootstrapRepos,
OpenPypeVersion
)
class UpdateThread(QThread):
"""Install Worker thread.
This class takes care of finding OpenPype version on user entered path
(or loading this path from database). If nothing is entered by user,
OpenPype will create its zip files from repositories that comes with it.
If path contains plain repositories, they are zipped and installed to
user data dir.
"""
progress = Signal(int)
message = Signal((str, bool))
def __init__(self, parent=None):
self._result = None
self._openpype_version = None
QThread.__init__(self, parent)
def set_version(self, openpype_version: OpenPypeVersion):
self._openpype_version = openpype_version
def result(self):
"""Result of finished installation."""
return self._result
def _set_result(self, value):
if self._result is not None:
raise AssertionError("BUG: Result was set more than once!")
self._result = value
def run(self):
"""Thread entry point.
Using :class:`BootstrapRepos` to either install OpenPype as zip files
or copy them from location specified by user or retrieved from
database.
"""
bs = BootstrapRepos(
progress_callback=self.set_progress, message=self.message)
version_path = bs.install_version(self._openpype_version)
self._set_result(version_path)
def set_progress(self, progress: int) -> None:
"""Helper to set progress bar.
Args:
progress (int): Progress in percents.
"""
self.progress.emit(progress)

136
igniter/update_window.py Normal file
View file

@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
"""Progress window to show when OpenPype is updating/installing locally."""
import os
from .update_thread import UpdateThread
from Qt import QtCore, QtGui, QtWidgets # noqa
from .bootstrap_repos import OpenPypeVersion
from .nice_progress_bar import NiceProgressBar
from .tools import load_stylesheet
class UpdateWindow(QtWidgets.QDialog):
"""OpenPype update window."""
_width = 500
_height = 100
def __init__(self, version: OpenPypeVersion, parent=None):
super(UpdateWindow, self).__init__(parent)
self._openpype_version = version
self._result_version_path = None
self.setWindowTitle(
f"OpenPype is updating ..."
)
self.setModal(True)
self.setWindowFlags(
QtCore.Qt.WindowMinimizeButtonHint
)
current_dir = os.path.dirname(os.path.abspath(__file__))
roboto_font_path = os.path.join(current_dir, "RobotoMono-Regular.ttf")
poppins_font_path = os.path.join(current_dir, "Poppins")
icon_path = os.path.join(current_dir, "openpype_icon.png")
# Install roboto font
QtGui.QFontDatabase.addApplicationFont(roboto_font_path)
for filename in os.listdir(poppins_font_path):
if os.path.splitext(filename)[1] == ".ttf":
QtGui.QFontDatabase.addApplicationFont(filename)
# Load logo
pixmap_openpype_logo = QtGui.QPixmap(icon_path)
# Set logo as icon of window
self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo))
self._pixmap_openpype_logo = pixmap_openpype_logo
self._update_thread = None
self.resize(QtCore.QSize(self._width, self._height))
self._init_ui()
# Set stylesheet
self.setStyleSheet(load_stylesheet())
self._run_update()
def _init_ui(self):
# Main info
# --------------------------------------------------------------------
main_label = QtWidgets.QLabel(
f"<b>OpenPype</b> is updating to {self._openpype_version}", self)
main_label.setWordWrap(True)
main_label.setObjectName("MainLabel")
# Progress bar
# --------------------------------------------------------------------
progress_bar = NiceProgressBar(self)
progress_bar.setAlignment(QtCore.Qt.AlignCenter)
progress_bar.setTextVisible(False)
# add all to main
main = QtWidgets.QVBoxLayout(self)
main.addSpacing(15)
main.addWidget(main_label, 0)
main.addSpacing(15)
main.addWidget(progress_bar, 0)
main.addSpacing(15)
self._progress_bar = progress_bar
def _run_update(self):
"""Start install process.
This will once again validate entered path and mongo if ok, start
working thread that will do actual job.
"""
# Check if install thread is not already running
if self._update_thread and self._update_thread.isRunning():
return
self._progress_bar.setRange(0, 0)
update_thread = UpdateThread(self)
update_thread.set_version(self._openpype_version)
update_thread.message.connect(self.update_console)
update_thread.progress.connect(self._update_progress)
update_thread.finished.connect(self._installation_finished)
self._update_thread = update_thread
update_thread.start()
def get_version_path(self):
return self._result_version_path
def _installation_finished(self):
status = self._update_thread.result()
self._result_version_path = status
self._progress_bar.setRange(0, 1)
self._update_progress(100)
QtWidgets.QApplication.processEvents()
self.done(0)
def _update_progress(self, progress: int):
# not updating progress as we are not able to determine it
# correctly now. Progress bar is set to un-deterministic mode
# until we are able to get progress in better way.
"""
self._progress_bar.setRange(0, 0)
self._progress_bar.setValue(progress)
text_visible = self._progress_bar.isTextVisible()
if progress == 0:
if text_visible:
self._progress_bar.setTextVisible(False)
elif not text_visible:
self._progress_bar.setTextVisible(True)
"""
return
def update_console(self, msg: str, error: bool = False) -> None:
"""Display message in console.
Args:
msg (str): message.
error (bool): if True, print it red.
"""
print(msg)

View file

@ -24,7 +24,9 @@ from .lib import (
get_latest_version,
get_global_environments,
get_local_site_id,
change_openpype_mongo_url
change_openpype_mongo_url,
create_project_folders,
get_project_basic_paths
)
from .lib.mongo import (
@ -72,6 +74,7 @@ __all__ = [
"get_current_project_settings",
"get_anatomy_settings",
"get_environments",
"get_project_basic_paths",
"SystemSettings",
@ -120,5 +123,9 @@ __all__ = [
"get_global_environments",
"get_local_site_id",
"change_openpype_mongo_url"
"change_openpype_mongo_url",
"get_project_basic_paths",
"create_project_folders"
]

View file

@ -18,6 +18,8 @@ from .pype_commands import PypeCommands
@click.option("--list-versions", is_flag=True, expose_value=False,
help=("list all detected versions. Use With `--use-staging "
"to list staging versions."))
@click.option("--validate-version", expose_value=False,
help="validate given version integrity")
def main(ctx):
"""Pype is main command serving as entry point to pipeline system.

View file

@ -10,8 +10,10 @@ from .pipeline import (
from avalon.tools import (
creator,
loader,
sceneinventory,
)
from openpype.tools import (
loader,
libraryloader
)

View file

@ -3,7 +3,7 @@ Basic avalon integration
"""
import os
from avalon.tools import workfiles
from openpype.tools import workfiles
from avalon import api as avalon
from pyblish import api as pyblish
from openpype.api import Logger

View file

@ -91,7 +91,8 @@ class ExtractRender(pyblish.api.InstancePlugin):
thumbnail_path = os.path.join(path, "thumbnail.png")
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
args = [
"{}".format(ffmpeg_path), "-y",
ffmpeg_path,
"-y",
"-i", os.path.join(path, list(collections[0])[0]),
"-vf", "scale=300:-1",
"-vframes", "1",

View file

@ -41,7 +41,8 @@ def menu_install():
apply_colorspace_project, apply_colorspace_clips
)
# here is the best place to add menu
from avalon.tools import cbloader, creator, sceneinventory
from avalon.tools import creator, sceneinventory
from openpype.tools import loader
from avalon.vendor.Qt import QtGui
menu_name = os.environ['AVALON_LABEL']
@ -90,7 +91,7 @@ def menu_install():
loader_action = menu.addAction("Load ...")
loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
loader_action.triggered.connect(cbloader.show)
loader_action.triggered.connect(loader.show)
sceneinventory_action = menu.addAction("Manage ...")
sceneinventory_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))

View file

@ -4,10 +4,8 @@ Basic avalon integration
import os
import contextlib
from collections import OrderedDict
from avalon.tools import (
workfiles,
publish as _publish
)
from avalon.tools import publish as _publish
from openpype.tools import workfiles
from avalon.pipeline import AVALON_CONTAINER_ID
from avalon import api as avalon
from avalon import schema

View file

@ -10,16 +10,16 @@ log = Logger().get_logger(__name__)
def tag_data():
return {
"Retiming": {
"editable": "1",
"note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", # noqa
"icon": "retiming.png",
"metadata": {
"family": "retiming",
"marginIn": 1,
"marginOut": 1
}
},
# "Retiming": {
# "editable": "1",
# "note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", # noqa
# "icon": "retiming.png",
# "metadata": {
# "family": "retiming",
# "marginIn": 1,
# "marginOut": 1
# }
# },
"[Lenses]": {
"Set lense here": {
"editable": "1",
@ -31,15 +31,15 @@ def tag_data():
}
}
},
"NukeScript": {
"editable": "1",
"note": "Collecting track items to Nuke scripts.",
"icon": "icons:TagNuke.png",
"metadata": {
"family": "nukescript",
"subset": "main"
}
},
# "NukeScript": {
# "editable": "1",
# "note": "Collecting track items to Nuke scripts.",
# "icon": "icons:TagNuke.png",
# "metadata": {
# "family": "nukescript",
# "subset": "main"
# }
# },
"Comment": {
"editable": "1",
"note": "Comment on a shot.",
@ -78,8 +78,7 @@ def update_tag(tag, data):
# set icon if any available in input data
if data.get("icon"):
tag.setIcon(str(data["icon"]))
# set note description of tag
tag.setNote(data["note"])
# get metadata of tag
mtd = tag.metadata()
# get metadata key from data
@ -97,6 +96,9 @@ def update_tag(tag, data):
"tag.{}".format(str(k)),
str(v)
)
# set note description of tag
tag.setNote(str(data["note"]))
return tag
@ -106,6 +108,26 @@ def add_tags_to_workfile():
"""
from .lib import get_current_project
def add_tag_to_bin(root_bin, name, data):
# for Tags to be created in root level Bin
# at first check if any of input data tag is not already created
done_tag = next((t for t in root_bin.items()
if str(name) in t.name()), None)
if not done_tag:
# create Tag
tag = create_tag(name, data)
tag.setName(str(name))
log.debug("__ creating tag: {}".format(tag))
# adding Tag to Root Bin
root_bin.addItem(tag)
else:
# update only non hierarchy tags
update_tag(done_tag, data)
done_tag.setName(str(name))
log.debug("__ updating tag: {}".format(done_tag))
# get project and root bin object
project = get_current_project()
root_bin = project.tagsBin()
@ -125,10 +147,8 @@ def add_tags_to_workfile():
for task_type in tasks.keys():
nks_pres_tags["[Tasks]"][task_type.lower()] = {
"editable": "1",
"note": "",
"icon": {
"path": "icons:TagGood.png"
},
"note": task_type,
"icon": "icons:TagGood.png",
"metadata": {
"family": "task",
"type": task_type
@ -157,10 +177,10 @@ def add_tags_to_workfile():
# check if key is not decorated with [] so it is defined as bin
bin_find = None
pattern = re.compile(r"\[(.*)\]")
bin_finds = pattern.findall(_k)
_bin_finds = pattern.findall(_k)
# if there is available any then pop it to string
if bin_finds:
bin_find = bin_finds.pop()
if _bin_finds:
bin_find = _bin_finds.pop()
# if bin was found then create or update
if bin_find:
@ -168,7 +188,6 @@ def add_tags_to_workfile():
# first check if in root lever is not already created bins
bins = [b for b in root_bin.items()
if b.name() in str(bin_find)]
log.debug(">>> bins: {}".format(bins))
if bins:
bin = bins.pop()
@ -178,49 +197,14 @@ def add_tags_to_workfile():
bin = hiero.core.Bin(str(bin_find))
# update or create tags in the bin
for k, v in _val.items():
tags = [t for t in bin.items()
if str(k) in t.name()
if len(str(k)) == len(t.name())]
if not tags:
# create Tag obj
tag = create_tag(k, v)
# adding Tag to Bin
bin.addItem(tag)
else:
update_tag(tags.pop(), v)
for __k, __v in _val.items():
add_tag_to_bin(bin, __k, __v)
# finally add the Bin object to the root level Bin
if root_add:
# adding Tag to Root Bin
root_bin.addItem(bin)
else:
# for Tags to be created in root level Bin
# at first check if any of input data tag is not already created
tags = None
tags = [t for t in root_bin.items()
if str(_k) in t.name()]
if not tags:
# create Tag
tag = create_tag(_k, _val)
# adding Tag to Root Bin
root_bin.addItem(tag)
else:
# update Tags if they already exists
for _t in tags:
# skip bin objects
if isinstance(_t, hiero.core.Bin):
continue
# check if Hierarchy in name and skip it
# because hierarchy could be edited
if "hierarchy" in _t.name().lower():
continue
# update only non hierarchy tags
update_tag(_t, _val)
add_tag_to_bin(root_bin, _k, _val)
log.info("Default Tags were set...")

View file

@ -378,6 +378,17 @@ def add_otio_metadata(otio_item, media_source, **kwargs):
def create_otio_timeline():
def set_prev_item(itemindex, track_item):
# Add Gap if needed
if itemindex == 0:
# if it is first track item at track then add
# it to previouse item
return track_item
else:
# get previouse item
return track_item.parent().items()[itemindex - 1]
# get current timeline
self.timeline = hiero.ui.activeSequence()
self.project_fps = self.timeline.framerate().toFloat()
@ -396,14 +407,6 @@ def create_otio_timeline():
type(track), track.name())
for itemindex, track_item in enumerate(track):
# skip offline track items
if not track_item.isMediaPresent():
continue
# skip if track item is disabled
if not track_item.isEnabled():
continue
# Add Gap if needed
if itemindex == 0:
# if it is first track item at track then add

View file

@ -5,7 +5,7 @@ import pyblish.api
class PreCollectClipEffects(pyblish.api.InstancePlugin):
"""Collect soft effects instances."""
order = pyblish.api.CollectorOrder - 0.579
order = pyblish.api.CollectorOrder - 0.479
label = "Precollect Clip Effects Instances"
families = ["clip"]

View file

@ -13,7 +13,7 @@ from pprint import pformat
class PrecollectInstances(pyblish.api.ContextPlugin):
"""Collect all Track items selection."""
order = pyblish.api.CollectorOrder - 0.59
order = pyblish.api.CollectorOrder - 0.49
label = "Precollect Instances"
hosts = ["hiero"]
@ -131,7 +131,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
self.create_shot_instance(context, **data)
self.log.info("Creating instance: {}".format(instance))
self.log.debug(
self.log.info(
"_ instance.data: {}".format(pformat(instance.data)))
if not with_audio:

View file

@ -8,11 +8,12 @@ from openpype.hosts.hiero.otio import hiero_export
from Qt.QtGui import QPixmap
import tempfile
class PrecollectWorkfile(pyblish.api.ContextPlugin):
"""Inject the current working file into context"""
label = "Precollect Workfile"
order = pyblish.api.CollectorOrder - 0.6
order = pyblish.api.CollectorOrder - 0.5
def process(self, context):

View file

@ -15,8 +15,8 @@ creator.show()
<scriptItem id="avalon_load">
<label>Load ...</label>
<scriptCode><![CDATA[
from avalon.tools import cbloader
cbloader.show(use_context=True)
from openpype.tools import loader
loader.show(use_context=True)
]]></scriptCode>
</scriptItem>

View file

@ -8,7 +8,7 @@ from avalon import api as avalon
from avalon import pipeline
from avalon.maya import suspended_refresh
from avalon.maya.pipeline import IS_HEADLESS
from avalon.tools import workfiles
from openpype.tools import workfiles
from pyblish import api as pyblish
from openpype.lib import any_outdated
import openpype.hosts.maya
@ -35,6 +35,7 @@ def install():
pyblish.register_plugin_path(PUBLISH_PATH)
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
log.info(PUBLISH_PATH)
menu.install()
@ -97,6 +98,7 @@ def uninstall():
pyblish.deregister_plugin_path(PUBLISH_PATH)
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
menu.uninstall()

View file

@ -79,7 +79,7 @@ def override_toolbox_ui():
log.warning("Could not import SceneInventory tool")
try:
import avalon.tools.loader as loader
import openpype.tools.loader as loader
except Exception:
log.warning("Could not import Loader tool")

View file

@ -4,6 +4,53 @@ import avalon.maya
from openpype.api import PypeCreatorMixin
def get_reference_node(members, log=None):
"""Get the reference node from the container members
Args:
members: list of node names
Returns:
str: Reference node name.
"""
from maya import cmds
# Collect the references without .placeHolderList[] attributes as
# unique entries (objects only) and skipping the sharedReferenceNode.
references = set()
for ref in cmds.ls(members, exactType="reference", objectsOnly=True):
# Ignore any `:sharedReferenceNode`
if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"):
continue
# Ignore _UNKNOWN_REF_NODE_ (PLN-160)
if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"):
continue
references.add(ref)
assert references, "No reference node found in container"
# Get highest reference node (least parents)
highest = min(references,
key=lambda x: len(get_reference_node_parents(x)))
# Warn the user when we're taking the highest reference node
if len(references) > 1:
if not log:
from openpype.lib import PypeLogger
log = PypeLogger().get_logger(__name__)
log.warning("More than one reference node found in "
"container, using highest reference node: "
"%s (in: %s)", highest, list(references))
return highest
def get_reference_node_parents(ref):
"""Return all parent reference nodes of reference node
@ -109,7 +156,7 @@ class ReferenceLoader(api.Loader):
loader=self.__class__.__name__
))
else:
ref_node = self._get_reference_node(nodes)
ref_node = get_reference_node(nodes, self.log)
loaded_containers.append(containerise(
name=name,
namespace=namespace,
@ -126,46 +173,6 @@ class ReferenceLoader(api.Loader):
"""To be implemented by subclass"""
raise NotImplementedError("Must be implemented by subclass")
def _get_reference_node(self, members):
"""Get the reference node from the container members
Args:
members: list of node names
Returns:
str: Reference node name.
"""
from maya import cmds
# Collect the references without .placeHolderList[] attributes as
# unique entries (objects only) and skipping the sharedReferenceNode.
references = set()
for ref in cmds.ls(members, exactType="reference", objectsOnly=True):
# Ignore any `:sharedReferenceNode`
if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"):
continue
# Ignore _UNKNOWN_REF_NODE_ (PLN-160)
if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"):
continue
references.add(ref)
assert references, "No reference node found in container"
# Get highest reference node (least parents)
highest = min(references,
key=lambda x: len(get_reference_node_parents(x)))
# Warn the user when we're taking the highest reference node
if len(references) > 1:
self.log.warning("More than one reference node found in "
"container, using highest reference node: "
"%s (in: %s)", highest, list(references))
return highest
def update(self, container, representation):
@ -178,7 +185,7 @@ class ReferenceLoader(api.Loader):
# Get reference node from container members
members = cmds.sets(node, query=True, nodesOnly=True)
reference_node = self._get_reference_node(members)
reference_node = get_reference_node(members, self.log)
file_type = {
"ma": "mayaAscii",
@ -274,7 +281,7 @@ class ReferenceLoader(api.Loader):
# Assume asset has been referenced
members = cmds.sets(node, query=True)
reference_node = self._get_reference_node(members)
reference_node = get_reference_node(members, self.log)
assert reference_node, ("Imported container not supported; "
"container must be referenced.")

View file

@ -31,7 +31,7 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget):
self.setObjectName("shaderDefinitionEditor")
self.setWindowTitle("OpenPype shader name definition editor")
icon = QtGui.QIcon(resources.pype_icon_filepath())
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(QtCore.Qt.Window)
self.setParent(parent)

View file

@ -9,3 +9,8 @@ class CreateSetDress(plugin.Creator):
family = "setdress"
icon = "cubes"
defaults = ["Main", "Anim"]
def __init__(self, *args, **kwargs):
super(CreateSetDress, self).__init__(*args, **kwargs)
self.data["exactSetMembersOnly"] = True

View file

@ -0,0 +1,92 @@
from avalon import api, io
class ImportModelRender(api.InventoryAction):
label = "Import Model Render Sets"
icon = "industry"
color = "#55DDAA"
scene_type_regex = "meta.render.m[ab]"
look_data_type = "meta.render.json"
@staticmethod
def is_compatible(container):
return (
container.get("loader") == "ReferenceLoader"
and container.get("name", "").startswith("model")
)
def process(self, containers):
from maya import cmds
for container in containers:
con_name = container["objectName"]
nodes = []
for n in cmds.sets(con_name, query=True, nodesOnly=True) or []:
if cmds.nodeType(n) == "reference":
nodes += cmds.referenceQuery(n, nodes=True)
else:
nodes.append(n)
repr_doc = io.find_one({
"_id": io.ObjectId(container["representation"]),
})
version_id = repr_doc["parent"]
print("Importing render sets for model %r" % con_name)
self.assign_model_render_by_version(nodes, version_id)
def assign_model_render_by_version(self, nodes, version_id):
"""Assign nodes a specific published model render data version by id.
This assumes the nodes correspond with the asset.
Args:
nodes(list): nodes to assign render data to
version_id (bson.ObjectId): database id of the version of model
Returns:
None
"""
import json
from maya import cmds
from avalon import maya, io, pipeline
from openpype.hosts.maya.api import lib
# Get representations of shader file and relationships
look_repr = io.find_one({
"type": "representation",
"parent": version_id,
"name": {"$regex": self.scene_type_regex},
})
if not look_repr:
print("No model render sets for this model version..")
return
json_repr = io.find_one({
"type": "representation",
"parent": version_id,
"name": self.look_data_type,
})
context = pipeline.get_representation_context(look_repr["_id"])
maya_file = pipeline.get_representation_path_from_context(context)
context = pipeline.get_representation_context(json_repr["_id"])
json_file = pipeline.get_representation_path_from_context(context)
# Import the look file
with maya.maintained_selection():
shader_nodes = cmds.file(maya_file,
i=True, # import
returnNewNodes=True)
# imprint context data
# Load relationships
shader_relation = json_file
with open(shader_relation, "r") as f:
relationships = json.load(f)
# Assign relationships
lib.apply_shaders(relationships, shader_nodes, nodes)

View file

@ -0,0 +1,29 @@
from maya import cmds
from avalon import api
from openpype.hosts.maya.api.plugin import get_reference_node
class ImportReference(api.InventoryAction):
"""Imports selected reference to inside of the file."""
label = "Import Reference"
icon = "download"
color = "#d8d8d8"
def process(self, containers):
references = cmds.ls(type="reference")
for container in containers:
if container["loader"] != "ReferenceLoader":
print("Not a reference, skipping")
continue
node = container["objectName"]
members = cmds.sets(node, query=True, nodesOnly=True)
ref_node = get_reference_node(members)
ref_file = cmds.referenceQuery(ref_node, f=True)
cmds.file(ref_file, importReference=True)
return True # return anything to trigger model refresh

View file

@ -223,8 +223,8 @@ class CollectLook(pyblish.api.InstancePlugin):
def process(self, instance):
"""Collect the Look in the instance with the correct layer settings"""
with lib.renderlayer(instance.data["renderlayer"]):
renderlayer = instance.data.get("renderlayer", "defaultRenderLayer")
with lib.renderlayer(renderlayer):
self.collect(instance)
def collect(self, instance):
@ -357,6 +357,23 @@ class CollectLook(pyblish.api.InstancePlugin):
for vray_node in vray_plugin_nodes:
history.extend(cmds.listHistory(vray_node))
# handling render attribute sets
render_set_types = [
"VRayDisplacement",
"VRayLightMesh",
"VRayObjectProperties",
"RedshiftObjectId",
"RedshiftMeshParameters",
]
render_sets = cmds.ls(look_sets, type=render_set_types)
if render_sets:
history.extend(
cmds.listHistory(render_sets,
future=False,
pruneDagObjects=True)
or []
)
files = cmds.ls(history, type="file", long=True)
files.extend(cmds.ls(history, type="aiImage", long=True))
files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True))
@ -550,3 +567,45 @@ class CollectLook(pyblish.api.InstancePlugin):
"source": source, # required for resources
"files": files,
"color_space": color_space} # required for resources
class CollectModelRenderSets(CollectLook):
"""Collect render attribute sets for model instance.
Collects additional render attribute sets so they can be
published with model.
"""
order = pyblish.api.CollectorOrder + 0.21
families = ["model"]
label = "Collect Model Render Sets"
hosts = ["maya"]
maketx = True
def collect_sets(self, instance):
"""Collect all related objectSets except shadingEngines
Args:
instance (list): all nodes to be published
Returns:
dict
"""
sets = {}
for node in instance:
related_sets = lib.get_related_sets(node)
if not related_sets:
continue
for objset in related_sets:
if objset in sets:
continue
if "shadingEngine" in cmds.nodeType(objset, inherited=True):
continue
sets[objset] = {"uuid": lib.get_id(objset), "members": list()}
return sets

View file

@ -122,7 +122,7 @@ def no_workspace_dir():
class ExtractLook(openpype.api.Extractor):
"""Extract Look (Maya Ascii + JSON)
"""Extract Look (Maya Scene + JSON)
Only extracts the sets (shadingEngines and alike) alongside a .json file
that stores it relationships for the sets and "attribute" data for the
@ -130,11 +130,12 @@ class ExtractLook(openpype.api.Extractor):
"""
label = "Extract Look (Maya ASCII + JSON)"
label = "Extract Look (Maya Scene + JSON)"
hosts = ["maya"]
families = ["look"]
order = pyblish.api.ExtractorOrder + 0.2
scene_type = "ma"
look_data_type = "json"
@staticmethod
def get_renderer_name():
@ -176,6 +177,8 @@ class ExtractLook(openpype.api.Extractor):
# no preset found
pass
return "mayaAscii" if self.scene_type == "ma" else "mayaBinary"
def process(self, instance):
"""Plugin entry point.
@ -183,10 +186,12 @@ class ExtractLook(openpype.api.Extractor):
instance: Instance to process.
"""
_scene_type = self.get_maya_scene_type(instance)
# Define extract output file path
dir_path = self.staging_dir(instance)
maya_fname = "{0}.{1}".format(instance.name, self.scene_type)
json_fname = "{0}.json".format(instance.name)
json_fname = "{0}.{1}".format(instance.name, self.look_data_type)
# Make texture dump folder
maya_path = os.path.join(dir_path, maya_fname)
@ -196,11 +201,100 @@ class ExtractLook(openpype.api.Extractor):
# Remove all members of the sets so they are not included in the
# exported file by accident
self.log.info("Extract sets (Maya ASCII) ...")
self.log.info("Extract sets (%s) ..." % _scene_type)
lookdata = instance.data["lookData"]
relationships = lookdata["relationships"]
sets = relationships.keys()
results = self.process_resources(instance, staging_dir=dir_path)
transfers = results["fileTransfers"]
hardlinks = results["fileHardlinks"]
hashes = results["fileHashes"]
remap = results["attrRemap"]
# Extract in correct render layer
layer = instance.data.get("renderlayer", "defaultRenderLayer")
with lib.renderlayer(layer):
# TODO: Ensure membership edits don't become renderlayer overrides
with lib.empty_sets(sets, force=True):
# To avoid Maya trying to automatically remap the file
# textures relative to the `workspace -directory` we force
# it to a fake temporary workspace. This fixes textures
# getting incorrectly remapped. (LKD-17, PLN-101)
with no_workspace_dir():
with lib.attribute_values(remap):
with avalon.maya.maintained_selection():
cmds.select(sets, noExpand=True)
cmds.file(
maya_path,
force=True,
typ=_scene_type,
exportSelected=True,
preserveReferences=False,
channels=True,
constraints=True,
expressions=True,
constructionHistory=True,
)
# Write the JSON data
self.log.info("Extract json..")
data = {
"attributes": lookdata["attributes"],
"relationships": relationships
}
with open(json_path, "w") as f:
json.dump(data, f)
if "files" not in instance.data:
instance.data["files"] = []
if "hardlinks" not in instance.data:
instance.data["hardlinks"] = []
if "transfers" not in instance.data:
instance.data["transfers"] = []
instance.data["files"].append(maya_fname)
instance.data["files"].append(json_fname)
if instance.data.get("representations") is None:
instance.data["representations"] = []
instance.data["representations"].append(
{
"name": self.scene_type,
"ext": self.scene_type,
"files": os.path.basename(maya_fname),
"stagingDir": os.path.dirname(maya_fname),
}
)
instance.data["representations"].append(
{
"name": self.look_data_type,
"ext": self.look_data_type,
"files": os.path.basename(json_fname),
"stagingDir": os.path.dirname(json_fname),
}
)
# Set up the resources transfers/links for the integrator
instance.data["transfers"].extend(transfers)
instance.data["hardlinks"].extend(hardlinks)
# Source hash for the textures
instance.data["sourceHashes"] = hashes
"""
self.log.info("Returning colorspaces to their original values ...")
for attr, value in remap.items():
self.log.info(" - {}: {}".format(attr, value))
cmds.setAttr(attr, value, type="string")
"""
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
maya_path))
def process_resources(self, instance, staging_dir):
# Extract the textures to transfer, possibly convert with maketx and
# remap the node paths to the destination path. Note that a source
# might be included more than once amongst the resources as they could
@ -218,7 +312,6 @@ class ExtractLook(openpype.api.Extractor):
color_space = resource.get("color_space")
for f in resource["files"]:
files_metadata[os.path.normpath(f)] = {
"color_space": color_space}
# files.update(os.path.normpath(f))
@ -244,7 +337,7 @@ class ExtractLook(openpype.api.Extractor):
source, mode, texture_hash = self._process_texture(
filepath,
do_maketx,
staging=dir_path,
staging=staging_dir,
linearize=linearize,
force=force_copy
)
@ -299,85 +392,13 @@ class ExtractLook(openpype.api.Extractor):
self.log.info("Finished remapping destinations ...")
# Extract in correct render layer
layer = instance.data.get("renderlayer", "defaultRenderLayer")
with lib.renderlayer(layer):
# TODO: Ensure membership edits don't become renderlayer overrides
with lib.empty_sets(sets, force=True):
# To avoid Maya trying to automatically remap the file
# textures relative to the `workspace -directory` we force
# it to a fake temporary workspace. This fixes textures
# getting incorrectly remapped. (LKD-17, PLN-101)
with no_workspace_dir():
with lib.attribute_values(remap):
with avalon.maya.maintained_selection():
cmds.select(sets, noExpand=True)
cmds.file(
maya_path,
force=True,
typ="mayaAscii",
exportSelected=True,
preserveReferences=False,
channels=True,
constraints=True,
expressions=True,
constructionHistory=True,
)
# Write the JSON data
self.log.info("Extract json..")
data = {
"attributes": lookdata["attributes"],
"relationships": relationships
return {
"fileTransfers": transfers,
"fileHardlinks": hardlinks,
"fileHashes": hashes,
"attrRemap": remap,
}
with open(json_path, "w") as f:
json.dump(data, f)
if "files" not in instance.data:
instance.data["files"] = []
if "hardlinks" not in instance.data:
instance.data["hardlinks"] = []
if "transfers" not in instance.data:
instance.data["transfers"] = []
instance.data["files"].append(maya_fname)
instance.data["files"].append(json_fname)
instance.data["representations"] = []
instance.data["representations"].append(
{
"name": "ma",
"ext": "ma",
"files": os.path.basename(maya_fname),
"stagingDir": os.path.dirname(maya_fname),
}
)
instance.data["representations"].append(
{
"name": "json",
"ext": "json",
"files": os.path.basename(json_fname),
"stagingDir": os.path.dirname(json_fname),
}
)
# Set up the resources transfers/links for the integrator
instance.data["transfers"].extend(transfers)
instance.data["hardlinks"].extend(hardlinks)
# Source hash for the textures
instance.data["sourceHashes"] = hashes
"""
self.log.info("Returning colorspaces to their original values ...")
for attr, value in remap.items():
self.log.info(" - {}: {}".format(attr, value))
cmds.setAttr(attr, value, type="string")
"""
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
maya_path))
def resource_destination(self, instance, filepath, do_maketx):
"""Get resource destination path.
@ -467,3 +488,26 @@ class ExtractLook(openpype.api.Extractor):
return converted, COPY, texture_hash
return filepath, COPY, texture_hash
class ExtractModelRenderSets(ExtractLook):
"""Extract model render attribute sets as model metadata
Only extracts the render attrib sets (NO shadingEngines) alongside
a .json file that stores it relationships for the sets and "attribute"
data for the instance members.
"""
label = "Model Render Sets"
hosts = ["maya"]
families = ["model"]
scene_type_prefix = "meta.render."
look_data_type = "meta.render.json"
def get_maya_scene_type(self, instance):
typ = super(ExtractModelRenderSets, self).get_maya_scene_type(instance)
# add prefix
self.scene_type = self.scene_type_prefix + self.scene_type
return typ

View file

@ -0,0 +1,25 @@
import pyblish.api
import openpype.api
class ValidateSetdressRoot(pyblish.api.InstancePlugin):
"""
"""
order = openpype.api.ValidateContentsOrder
label = "SetDress Root"
hosts = ["maya"]
families = ["setdress"]
def process(self, instance):
from maya import cmds
if instance.data.get("exactSetMembersOnly"):
return
set_member = instance.data["setMembers"]
root = cmds.ls(set_member, assemblies=True, long=True)
if not root or root[0] not in set_member:
raise Exception("Setdress top root node is not being published.")

View file

@ -7,7 +7,7 @@ from collections import OrderedDict
from avalon import api, io, lib
from avalon.tools import workfiles
from openpype.tools import workfiles
import avalon.nuke
from avalon.nuke import lib as anlib
from avalon.nuke import (
@ -287,7 +287,7 @@ def script_name():
def add_button_write_to_read(node):
name = "createReadNode"
label = "Cread Read From Rendered"
label = "Create Read From Rendered"
value = "import write_to_read;write_to_read.write_to_read(nuke.thisNode())"
knob = nuke.PyScript_Knob(name, label, value)
knob.clearFlag(nuke.STARTLINE)
@ -727,7 +727,7 @@ class WorkfileSettings(object):
log.error(msg)
nuke.message(msg)
log.warning(">> root_dict: {}".format(root_dict))
log.debug(">> root_dict: {}".format(root_dict))
# first set OCIO
if self._root_node["colorManagement"].value() \
@ -1277,6 +1277,7 @@ class ExporterReview:
def clean_nodes(self):
for node in self._temp_nodes:
nuke.delete(node)
self._temp_nodes = []
self.log.info("Deleted nodes...")
@ -1301,6 +1302,7 @@ class ExporterReviewLut(ExporterReview):
lut_style=None):
# initialize parent class
ExporterReview.__init__(self, klass, instance)
self._temp_nodes = []
# deal with now lut defined in viewer lut
if hasattr(klass, "viewer_lut_raw"):

View file

@ -76,6 +76,8 @@ class LoadSequence(api.Loader):
file = file.replace("\\", "/")
repr_cont = context["representation"]["context"]
assert repr_cont.get("frame"), "Representation is not sequence"
if "#" not in file:
frame = repr_cont.get("frame")
if frame:
@ -170,6 +172,7 @@ class LoadSequence(api.Loader):
assert read_node.Class() == "Read", "Must be Read"
repr_cont = representation["context"]
assert repr_cont.get("frame"), "Representation is not sequence"
file = api.get_representation_path(representation)

View file

@ -2,6 +2,7 @@ import nuke
import pyblish.api
from avalon.nuke import maintained_selection
class CreateOutputNode(pyblish.api.ContextPlugin):
"""Adding output node for each ouput write node
So when latly user will want to Load .nk as LifeGroup or Precomp
@ -15,8 +16,8 @@ class CreateOutputNode(pyblish.api.ContextPlugin):
def process(self, context):
# capture selection state
with maintained_selection():
active_node = [node for inst in context[:]
for node in inst[:]
active_node = [node for inst in context
for node in inst
if "ak:family" in node.knobs()]
if active_node:

View file

@ -3,6 +3,12 @@ import pyblish.api
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import lib as pnlib
import openpype
try:
from __builtin__ import reload
except ImportError:
from importlib import reload
reload(pnlib)

View file

@ -4,6 +4,13 @@ from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import lib as pnlib
import openpype
try:
from __builtin__ import reload
except ImportError:
from importlib import reload
reload(pnlib)
class ExtractReviewDataMov(openpype.api.Extractor):
"""Extracts movie and thumbnail with baked in luts

View file

@ -1,3 +1,4 @@
import sys
import os
import nuke
from avalon.nuke import lib as anlib
@ -5,6 +6,10 @@ import pyblish.api
import openpype
if sys.version_info[0] >= 3:
unicode = str
class ExtractThumbnail(openpype.api.Extractor):
"""Extracts movie and thumbnail with baked in luts
@ -112,24 +117,26 @@ class ExtractThumbnail(openpype.api.Extractor):
# create write node
write_node = nuke.createNode("Write")
file = fhead + "jpeg"
file = fhead + "jpg"
name = "thumbnail"
path = os.path.join(staging_dir, file).replace("\\", "/")
instance.data["thumbnail"] = path
write_node["file"].setValue(path)
write_node["file_type"].setValue("jpeg")
write_node["file_type"].setValue("jpg")
write_node["raw"].setValue(1)
write_node.setInput(0, previous_node)
temporary_nodes.append(write_node)
tags = ["thumbnail", "publish_on_farm"]
# retime for
mid_frame = int((int(last_frame) - int(first_frame)) / 2) \
+ int(first_frame)
first_frame = int(last_frame) / 2
last_frame = int(last_frame) / 2
repre = {
'name': name,
'ext': "jpeg",
'ext': "jpg",
"outputName": "thumb",
'files': file,
"stagingDir": staging_dir,
@ -140,7 +147,7 @@ class ExtractThumbnail(openpype.api.Extractor):
instance.data["representations"].append(repre)
# Render frames
nuke.execute(write_node.name(), int(first_frame), int(last_frame))
nuke.execute(write_node.name(), int(mid_frame), int(mid_frame))
self.log.debug(
"representations: {}".format(instance.data["representations"]))

View file

@ -9,7 +9,7 @@ class IncrementScriptVersion(pyblish.api.ContextPlugin):
order = pyblish.api.IntegratorOrder + 0.9
label = "Increment Script Version"
optional = True
families = ["workfile", "render", "render.local", "render.farm"]
families = ["workfile"]
hosts = ['nuke']
def process(self, context):

View file

@ -8,12 +8,12 @@ from avalon.nuke import lib as anlib
class PreCollectNukeInstances(pyblish.api.ContextPlugin):
"""Collect all nodes with Avalon knob."""
order = pyblish.api.CollectorOrder - 0.59
order = pyblish.api.CollectorOrder - 0.49
label = "Pre-collect Instances"
hosts = ["nuke", "nukeassist"]
# presets
sync_workfile_version = False
sync_workfile_version_on_families = []
def process(self, context):
asset_data = io.find_one({
@ -120,11 +120,12 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin):
# sync workfile version
_families_test = [family] + families
self.log.debug("__ _families_test: `{}`".format(_families_test))
if not next((f for f in _families_test
if "prerender" in f),
None) and self.sync_workfile_version:
# get version to instance for integration
instance.data['version'] = instance.context.data['version']
for family_test in _families_test:
if family_test in self.sync_workfile_version_on_families:
self.log.debug("Syncing version with workfile for '{}'"
.format(family_test))
# get version to instance for integration
instance.data['version'] = instance.context.data['version']
instance.data.update({
"subset": subset,

View file

@ -3,13 +3,12 @@ import pyblish.api
import os
import openpype.api as pype
from avalon.nuke import lib as anlib
reload(anlib)
class CollectWorkfile(pyblish.api.ContextPlugin):
"""Collect current script for publish."""
order = pyblish.api.CollectorOrder - 0.60
order = pyblish.api.CollectorOrder - 0.50
label = "Pre-collect Workfile"
hosts = ['nuke']

View file

@ -11,7 +11,7 @@ from avalon import io, api
class CollectNukeWrites(pyblish.api.InstancePlugin):
"""Collect all write nodes."""
order = pyblish.api.CollectorOrder - 0.58
order = pyblish.api.CollectorOrder - 0.48
label = "Pre-collect Writes"
hosts = ["nuke", "nukeassist"]
families = ["write"]

View file

@ -0,0 +1,33 @@
import pyblish
import nuke
class FixProxyMode(pyblish.api.Action):
"""
Togger off proxy switch OFF
"""
label = "Proxy toggle to OFF"
icon = "wrench"
on = "failed"
def process(self, context, plugin):
rootNode = nuke.root()
rootNode["proxy"].setValue(False)
@pyblish.api.log
class ValidateProxyMode(pyblish.api.ContextPlugin):
"""Validate active proxy mode"""
order = pyblish.api.ValidatorOrder
label = "Validate Proxy Mode"
hosts = ["nuke"]
actions = [FixProxyMode]
def process(self, context):
rootNode = nuke.root()
isProxy = rootNode["proxy"].value()
assert not isProxy, "Proxy mode should be toggled OFF"

View file

@ -69,7 +69,8 @@ def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame):
frames = sorted(frames)
firstframe = frames[0]
lastframe = frames[len(frames) - 1]
if lastframe < 0:
if int(lastframe) < 0:
lastframe = firstframe
return filepath, firstframe, lastframe

View file

@ -60,7 +60,8 @@ class ExtractReview(openpype.api.Extractor):
# Generate thumbnail.
thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg")
args = [
"{}".format(ffmpeg_path), "-y",
ffmpeg_path,
"-y",
"-i", output_image_path,
"-vf", "scale=300:-1",
"-vframes", "1",
@ -78,7 +79,8 @@ class ExtractReview(openpype.api.Extractor):
# Generate mov.
mov_path = os.path.join(staging_dir, "review.mov")
args = [
ffmpeg_path, "-y",
ffmpeg_path,
"-y",
"-i", output_image_path,
"-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
"-vframes", "1",

View file

@ -10,11 +10,13 @@ from .pipeline import (
from avalon.tools import (
creator,
loader,
sceneinventory,
libraryloader,
subsetmanager
)
from openpype.tools import (
loader,
libraryloader,
)
def load_stylesheet():

View file

@ -4,7 +4,7 @@ Basic avalon integration
import os
import contextlib
from collections import OrderedDict
from avalon.tools import workfiles
from openpype.tools import workfiles
from avalon import api as avalon
from avalon import schema
from avalon.pipeline import AVALON_CONTAINER_ID

View file

@ -8,7 +8,7 @@ from pprint import pformat
class PrecollectInstances(pyblish.api.ContextPlugin):
"""Collect all Track items selection."""
order = pyblish.api.CollectorOrder - 0.59
order = pyblish.api.CollectorOrder - 0.49
label = "Precollect Instances"
hosts = ["resolve"]

View file

@ -13,7 +13,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
"""Precollect the current working file into context"""
label = "Precollect Workfile"
order = pyblish.api.CollectorOrder - 0.6
order = pyblish.api.CollectorOrder - 0.5
def process(self, context):

View file

@ -11,6 +11,7 @@ import zipfile
import pyblish.api
from avalon import api, io
import openpype.api
from openpype.lib import get_workfile_template_key_from_context
class ExtractHarmonyZip(openpype.api.Extractor):
@ -65,10 +66,10 @@ class ExtractHarmonyZip(openpype.api.Extractor):
# Get Task types and Statuses for creation if needed
self.task_types = self._get_all_task_types(project_entity)
self.task_statuses = self.get_all_task_statuses(project_entity)
self.task_statuses = self._get_all_task_statuses(project_entity)
# Get Statuses of AssetVersions
self.assetversion_statuses = self.get_all_assetversion_statuses(
self.assetversion_statuses = self._get_all_assetversion_statuses(
project_entity
)
@ -233,18 +234,28 @@ class ExtractHarmonyZip(openpype.api.Extractor):
"version": 1,
"ext": "zip",
}
host_name = "harmony"
template_name = get_workfile_template_key_from_context(
instance.data["asset"],
instance.data.get("task"),
host_name,
project_name=project_entity["name"],
dbcon=io
)
# Get a valid work filename first with version 1
file_template = anatomy.templates["work"]["file"]
file_template = anatomy.templates[template_name]["file"]
anatomy_filled = anatomy.format(data)
work_path = anatomy_filled["work"]["path"]
work_path = anatomy_filled[template_name]["path"]
# Get the final work filename with the proper version
data["version"] = api.last_workfile_with_version(
os.path.dirname(work_path), file_template, data, [".zip"]
os.path.dirname(work_path),
file_template,
data,
api.HOST_WORKFILE_EXTENSIONS[host_name]
)[1]
work_path = anatomy_filled["work"]["path"]
base_name = os.path.splitext(os.path.basename(work_path))[0]
staging_work_path = os.path.join(os.path.dirname(staging_scene),

View file

@ -58,7 +58,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
# use first frame as thumbnail if is sequence of jpegs
full_thumbnail_path = os.path.join(
thumbnail_repre["stagingDir"], file
)
)
self.log.info(
"For thumbnail is used file: {}".format(full_thumbnail_path)
)
@ -116,7 +116,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
# create new thumbnail representation
representation = {
'name': 'jpg',
'name': 'thumbnail',
'ext': 'jpg',
'files': filename,
"stagingDir": staging_dir,

View file

@ -59,32 +59,35 @@ class ExtractTrimVideoAudio(openpype.api.Extractor):
if "trimming" not in fml
]
args = [
f"\"{ffmpeg_path}\"",
ffmpeg_args = [
ffmpeg_path,
"-ss", str(start / fps),
"-i", f"\"{video_file_path}\"",
"-i", video_file_path,
"-t", str(dur / fps)
]
if ext in [".mov", ".mp4"]:
args.extend([
ffmpeg_args.extend([
"-crf", "18",
"-pix_fmt", "yuv420p"])
"-pix_fmt", "yuv420p"
])
elif ext in ".wav":
args.extend([
"-vn -acodec pcm_s16le",
"-ar 48000 -ac 2"
ffmpeg_args.extend([
"-vn",
"-acodec", "pcm_s16le",
"-ar", "48000",
"-ac", "2"
])
# add output path
args.append(f"\"{clip_trimed_path}\"")
ffmpeg_args.append(clip_trimed_path)
self.log.info(f"Processing: {args}")
ffmpeg_args = " ".join(args)
joined_args = " ".join(ffmpeg_args)
self.log.info(f"Processing: {joined_args}")
openpype.api.run_subprocess(
ffmpeg_args, shell=True, logger=self.log
ffmpeg_args, logger=self.log
)
repr = {
repre = {
"name": ext[1:],
"ext": ext[1:],
"files": os.path.basename(clip_trimed_path),
@ -97,10 +100,10 @@ class ExtractTrimVideoAudio(openpype.api.Extractor):
}
if ext in [".mov", ".mp4"]:
repr.update({
repre.update({
"thumbnail": True,
"tags": ["review", "ftrackreview", "delete"]})
instance.data["representations"].append(repr)
instance.data["representations"].append(repre)
self.log.debug(f"Instance data: {pformat(instance.data)}")

View file

@ -10,6 +10,7 @@ Provides:
import os
import json
import clique
import tempfile
import pyblish.api
from avalon import io
@ -94,7 +95,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
instance.data["families"] = families
instance.data["version"] = \
self._get_last_version(asset, subset) + 1
instance.data["stagingDir"] = task_dir
instance.data["stagingDir"] = tempfile.mkdtemp()
instance.data["source"] = "webpublisher"
# to store logging info into DB openpype.webpublishes
@ -113,6 +114,8 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
instance.data["frameEnd"] = \
instance.data["representations"][0]["frameEnd"]
else:
instance.data["frameStart"] = 0
instance.data["frameEnd"] = 1
instance.data["representations"] = self._get_single_repre(
task_dir, task_data["files"], tags
)
@ -174,7 +177,11 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
(family, [families], subset_template_name, tags) tuple
AssertionError if not matching family found
"""
task_obj = settings.get(task_type)
task_type = task_type.lower()
lower_cased_task_types = {}
for t_type, task in settings.items():
lower_cased_task_types[t_type.lower()] = task
task_obj = lower_cased_task_types.get(task_type)
assert task_obj, "No family configuration for '{}'".format(task_type)
found_family = None

View file

@ -27,6 +27,7 @@ from .execute import (
get_pype_execute_args,
execute,
run_subprocess,
path_to_subprocess_arg,
CREATE_NO_WINDOW
)
from .log import PypeLogger, timeit
@ -59,6 +60,11 @@ from .python_module_tools import (
import_module_from_dirpath
)
from .profiles_filtering import (
compile_list_of_regexes,
filter_profiles
)
from .avalon_context import (
CURRENT_DOC_SCHEMAS,
PROJECT_NAME_ALLOWED_SYMBOLS,
@ -118,13 +124,9 @@ from .applications import (
prepare_host_environments,
prepare_context_environments,
get_app_environments_for_context,
apply_project_environments_value,
compile_list_of_regexes
apply_project_environments_value
)
from .profiles_filtering import filter_profiles
from .plugin_tools import (
TaskNotSetError,
get_subset_name,
@ -143,7 +145,9 @@ from .plugin_tools import (
from .path_tools import (
version_up,
get_version_from_path,
get_last_version_from_path
get_last_version_from_path,
create_project_folders,
get_project_basic_paths
)
from .editorial import (
@ -158,12 +162,19 @@ from .editorial import (
make_sequence_collection
)
from .pype_info import (
get_openpype_version,
get_build_version
)
terminal = Terminal
__all__ = [
"get_pype_execute_args",
"execute",
"run_subprocess",
"path_to_subprocess_arg",
"CREATE_NO_WINDOW",
"env_value_to_bool",
"get_paths_from_environ",
@ -276,5 +287,10 @@ __all__ = [
"range_from_frames",
"frames_to_secons",
"frames_to_timecode",
"make_sequence_collection"
"make_sequence_collection",
"create_project_folders",
"get_project_basic_paths",
"get_openpype_version",
"get_build_version",
]

View file

@ -25,11 +25,12 @@ from . import (
PypeLogger,
Anatomy
)
from .profiles_filtering import filter_profiles
from .local_settings import get_openpype_username
from .avalon_context import (
get_workdir_data,
get_workdir_with_workdir_data,
get_workfile_template_key_from_context
get_workfile_template_key
)
from .python_module_tools import (
@ -1226,8 +1227,12 @@ def prepare_context_environments(data):
# Load project specific environments
project_name = project_doc["name"]
project_settings = get_project_settings(project_name)
data["project_settings"] = project_settings
# Apply project specific environments on current env value
apply_project_environments_value(project_name, data["env"])
apply_project_environments_value(
project_name, data["env"], project_settings
)
app = data["app"]
workdir_data = get_workdir_data(
@ -1237,17 +1242,22 @@ def prepare_context_environments(data):
anatomy = data["anatomy"]
template_key = get_workfile_template_key_from_context(
asset_doc["name"],
task_name,
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
task_info = asset_tasks.get(task_name) or {}
task_type = task_info.get("type")
# Temp solution how to pass task type to `_prepare_last_workfile`
data["task_type"] = task_type
workfile_template_key = get_workfile_template_key(
task_type,
app.host_name,
project_name=project_name,
dbcon=data["dbcon"]
project_settings=project_settings
)
try:
workdir = get_workdir_with_workdir_data(
workdir_data, anatomy, template_key=template_key
workdir_data, anatomy, template_key=workfile_template_key
)
except Exception as exc:
@ -1281,10 +1291,10 @@ def prepare_context_environments(data):
)
data["env"].update(context_env)
_prepare_last_workfile(data, workdir)
_prepare_last_workfile(data, workdir, workfile_template_key)
def _prepare_last_workfile(data, workdir):
def _prepare_last_workfile(data, workdir, workfile_template_key):
"""last workfile workflow preparation.
Function check if should care about last workfile workflow and tries
@ -1314,13 +1324,14 @@ def _prepare_last_workfile(data, workdir):
workdir_data = copy.deepcopy(_workdir_data)
project_name = data["project_name"]
task_name = data["task_name"]
task_type = data["task_type"]
start_last_workfile = should_start_last_workfile(
project_name, app.host_name, task_name
project_name, app.host_name, task_name, task_type
)
data["start_last_workfile"] = start_last_workfile
workfile_startup = should_workfile_tool_start(
project_name, app.host_name, task_name
project_name, app.host_name, task_name, task_type
)
data["workfile_startup"] = workfile_startup
@ -1345,7 +1356,7 @@ def _prepare_last_workfile(data, workdir):
if extensions:
anatomy = data["anatomy"]
# Find last workfile
file_template = anatomy.templates["work"]["file"]
file_template = anatomy.templates[workfile_template_key]["file"]
workdir_data.update({
"version": 1,
"user": get_openpype_username(),
@ -1369,54 +1380,8 @@ def _prepare_last_workfile(data, workdir):
data["last_workfile_path"] = last_workfile_path
def get_option_from_settings(
startup_presets, host_name, task_name, default_output
):
host_name_lowered = host_name.lower()
task_name_lowered = task_name.lower()
max_points = 2
matching_points = -1
matching_item = None
for item in startup_presets:
hosts = item.get("hosts") or tuple()
tasks = item.get("tasks") or tuple()
hosts_lowered = tuple(_host_name.lower() for _host_name in hosts)
# Skip item if has set hosts and current host is not in
if hosts_lowered and host_name_lowered not in hosts_lowered:
continue
tasks_lowered = tuple(_task_name.lower() for _task_name in tasks)
# Skip item if has set tasks and current task is not in
if tasks_lowered:
task_match = False
for task_regex in compile_list_of_regexes(tasks_lowered):
if re.match(task_regex, task_name_lowered):
task_match = True
break
if not task_match:
continue
points = int(bool(hosts_lowered)) + int(bool(tasks_lowered))
if points > matching_points:
matching_item = item
matching_points = points
if matching_points == max_points:
break
if matching_item is not None:
output = matching_item.get("enabled")
if output is None:
output = default_output
return output
return default_output
def should_start_last_workfile(
project_name, host_name, task_name, default_output=False
project_name, host_name, task_name, task_type, default_output=False
):
"""Define if host should start last version workfile if possible.
@ -1438,7 +1403,7 @@ def should_start_last_workfile(
"""
project_settings = get_project_settings(project_name)
startup_presets = (
profiles = (
project_settings
["global"]
["tools"]
@ -1446,15 +1411,27 @@ def should_start_last_workfile(
["last_workfile_on_startup"]
)
if not startup_presets:
if not profiles:
return default_output
return get_option_from_settings(
startup_presets, host_name, task_name, default_output)
filter_data = {
"tasks": task_name,
"task_types": task_type,
"hosts": host_name
}
matching_item = filter_profiles(profiles, filter_data)
output = None
if matching_item:
output = matching_item.get("enabled")
if output is None:
return default_output
return output
def should_workfile_tool_start(
project_name, host_name, task_name, default_output=False
project_name, host_name, task_name, task_type, default_output=False
):
"""Define if host should start workfile tool at host launch.
@ -1476,7 +1453,7 @@ def should_workfile_tool_start(
"""
project_settings = get_project_settings(project_name)
startup_presets = (
profiles = (
project_settings
["global"]
["tools"]
@ -1484,27 +1461,20 @@ def should_workfile_tool_start(
["open_workfile_tool_on_startup"]
)
if not startup_presets:
if not profiles:
return default_output
return get_option_from_settings(
startup_presets, host_name, task_name, default_output)
filter_data = {
"tasks": task_name,
"task_types": task_type,
"hosts": host_name
}
matching_item = filter_profiles(profiles, filter_data)
output = None
if matching_item:
output = matching_item.get("enabled")
def compile_list_of_regexes(in_list):
"""Convert strings in entered list to compiled regex objects."""
regexes = list()
if not in_list:
return regexes
for item in in_list:
if not item:
continue
try:
regexes.append(re.compile(item))
except TypeError:
print((
"Invalid type \"{}\" value \"{}\"."
" Expected string based object. Skipping."
).format(str(type(item)), str(item)))
return regexes
if output is None:
return default_output
return output

View file

@ -10,6 +10,7 @@ import functools
from openpype.settings import get_project_settings
from .anatomy import Anatomy
from .profiles_filtering import filter_profiles
# avalon module is not imported at the top
# - may not be in path at the time of pype.lib initialization
@ -453,8 +454,6 @@ def get_workfile_template_key(
if not profiles:
return default
from .profiles_filtering import filter_profiles
profile_filter = {
"task_types": task_type,
"hosts": host_name
@ -791,7 +790,9 @@ class BuildWorkfile:
current_task_name = avalon.io.Session["AVALON_TASK"]
# Load workfile presets for task
self.build_presets = self.get_build_presets(current_task_name)
self.build_presets = self.get_build_presets(
current_task_name, current_asset_entity
)
# Skip if there are any presets for task
if not self.build_presets:
@ -875,7 +876,7 @@ class BuildWorkfile:
return loaded_containers
@with_avalon
def get_build_presets(self, task_name):
def get_build_presets(self, task_name, asset_doc):
""" Returns presets to build workfile for task name.
Presets are loaded for current project set in
@ -889,30 +890,33 @@ class BuildWorkfile:
(dict): preset per entered task name
"""
host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1]
presets = get_project_settings(avalon.io.Session["AVALON_PROJECT"])
project_settings = get_project_settings(
avalon.io.Session["AVALON_PROJECT"]
)
host_settings = project_settings.get(host_name) or {}
# Get presets for host
wb_settings = presets.get(host_name, {}).get("workfile_builder")
wb_settings = host_settings.get("workfile_builder")
if not wb_settings:
# backward compatibility
wb_settings = presets.get(host_name, {}).get("workfile_build")
wb_settings = host_settings.get("workfile_build") or {}
builder_presets = wb_settings.get("profiles")
builder_profiles = wb_settings.get("profiles")
if not builder_profiles:
return None
if not builder_presets:
return
task_name_low = task_name.lower()
per_task_preset = None
for preset in builder_presets:
preset_tasks = preset.get("tasks") or []
preset_tasks_low = [task.lower() for task in preset_tasks]
if task_name_low in preset_tasks_low:
per_task_preset = preset
break
return per_task_preset
task_type = (
asset_doc
.get("data", {})
.get("tasks", {})
.get(task_name, {})
.get("type")
)
filter_data = {
"task_types": task_type,
"tasks": task_name
}
return filter_profiles(builder_profiles, filter_data)
def _filter_build_profiles(self, build_profiles, loaders_by_name):
""" Filter build profiles by loaders and prepare process data.

View file

@ -1,11 +1,10 @@
import logging
import os
import shlex
import subprocess
import platform
from .log import PypeLogger as Logger
log = logging.getLogger(__name__)
# MSDN process creation flag (Windows only)
CREATE_NO_WINDOW = 0x08000000
@ -100,7 +99,9 @@ def run_subprocess(*args, **kwargs):
filtered_env = {str(k): str(v) for k, v in env.items()}
# Use lib's logger if was not passed with kwargs.
logger = kwargs.pop("logger", log)
logger = kwargs.pop("logger", None)
if logger is None:
logger = Logger.get_logger("run_subprocess")
# set overrides
kwargs['stdout'] = kwargs.get('stdout', subprocess.PIPE)
@ -138,6 +139,14 @@ def run_subprocess(*args, **kwargs):
return full_output
def path_to_subprocess_arg(path):
"""Prepare path for subprocess arguments.
Returned path can be wrapped with quotes or kept as is.
"""
return subprocess.list2cmdline([path])
def get_pype_execute_args(*args):
"""Arguments to run pype command.

View file

@ -1,6 +1,11 @@
import json
import logging
import os
import re
import logging
from .anatomy import Anatomy
from openpype.settings import get_project_settings
log = logging.getLogger(__name__)
@ -77,7 +82,7 @@ def get_version_from_path(file):
"""
pattern = re.compile(r"[\._]v([0-9]+)", re.IGNORECASE)
try:
return pattern.findall(file)[0]
return pattern.findall(file)[-1]
except IndexError:
log.error(
"templates:get_version_from_workfile:"
@ -119,3 +124,75 @@ def get_last_version_from_path(path_dir, filter):
return filtred_files[-1]
return None
def compute_paths(basic_paths_items, project_root):
pattern_array = re.compile(r"\[.*\]")
project_root_key = "__project_root__"
output = []
for path_items in basic_paths_items:
clean_items = []
for path_item in path_items:
matches = re.findall(pattern_array, path_item)
if len(matches) > 0:
path_item = path_item.replace(matches[0], "")
if path_item == project_root_key:
path_item = project_root
clean_items.append(path_item)
output.append(os.path.normpath(os.path.sep.join(clean_items)))
return output
def create_project_folders(basic_paths, project_name):
anatomy = Anatomy(project_name)
roots_paths = []
if isinstance(anatomy.roots, dict):
for root in anatomy.roots.values():
roots_paths.append(root.value)
else:
roots_paths.append(anatomy.roots.value)
for root_path in roots_paths:
project_root = os.path.join(root_path, project_name)
full_paths = compute_paths(basic_paths, project_root)
# Create folders
for path in full_paths:
full_path = path.format(project_root=project_root)
if os.path.exists(full_path):
log.debug(
"Folder already exists: {}".format(full_path)
)
else:
log.debug("Creating folder: {}".format(full_path))
os.makedirs(full_path)
def _list_path_items(folder_structure):
output = []
for key, value in folder_structure.items():
if not value:
output.append(key)
else:
paths = _list_path_items(value)
for path in paths:
if not isinstance(path, (list, tuple)):
path = [path]
item = [key]
item.extend(path)
output.append(item)
return output
def get_project_basic_paths(project_name):
project_settings = get_project_settings(project_name)
folder_structure = (
project_settings["global"]["project_folder_structure"]
)
if not folder_structure:
return []
if isinstance(folder_structure, str):
folder_structure = json.loads(folder_structure)
return _list_path_items(folder_structure)

View file

@ -35,7 +35,8 @@ def get_subset_name(
project_name=None,
host_name=None,
default_template=None,
dynamic_data=None
dynamic_data=None,
dbcon=None
):
if not family:
return ""
@ -46,13 +47,42 @@ def get_subset_name(
# Use only last part of class family value split by dot (`.`)
family = family.rsplit(".", 1)[-1]
if project_name is None:
import avalon.api
project_name = avalon.api.Session["AVALON_PROJECT"]
# Function should expect asset document instead of asset id
# - that way `dbcon` is not needed
if dbcon is None:
from avalon.api import AvalonMongoDB
dbcon = AvalonMongoDB()
dbcon.Session["AVALON_PROJECT"] = project_name
dbcon.install()
asset_doc = dbcon.find_one(
{
"type": "asset",
"_id": asset_id
},
{
"data.tasks": True
}
)
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
task_info = asset_tasks.get(task_name) or {}
task_type = task_info.get("type")
# Get settings
tools_settings = get_project_settings(project_name)["global"]["tools"]
profiles = tools_settings["creator"]["subset_name_profiles"]
filtering_criteria = {
"families": family,
"hosts": host_name,
"tasks": task_name
"tasks": task_name,
"task_types": task_type
}
matching_profile = filter_profiles(profiles, filtering_criteria)

View file

@ -1,10 +1,28 @@
import re
import logging
from .applications import compile_list_of_regexes
log = logging.getLogger(__name__)
def compile_list_of_regexes(in_list):
"""Convert strings in entered list to compiled regex objects."""
regexes = list()
if not in_list:
return regexes
for item in in_list:
if not item:
continue
try:
regexes.append(re.compile(item))
except TypeError:
print((
"Invalid type \"{}\" value \"{}\"."
" Expected string based object. Skipping."
).format(str(type(item)), str(item)))
return regexes
def _profile_exclusion(matching_profiles, logger):
"""Find out most matching profile byt host, task and family match.
@ -165,7 +183,8 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None):
if match == -1:
profile_value = profile.get(key) or []
logger.debug(
"\"{}\" not found in {}".format(key, profile_value)
"\"{}\" not found in \"{}\": {}".format(value, key,
profile_value)
)
profile_points = -1
break
@ -192,13 +211,13 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None):
])
if not matching_profiles:
logger.warning(
logger.info(
"None of profiles match your setup. {}".format(log_parts)
)
return None
if len(matching_profiles) > 1:
logger.warning(
logger.info(
"More than one profile match your setup. {}".format(log_parts)
)

View file

@ -9,23 +9,76 @@ import openpype.version
from openpype.settings.lib import get_local_settings
from .execute import get_pype_execute_args
from .local_settings import get_local_site_id
from .python_module_tools import import_filepath
def get_openpype_version():
"""Version of pype that is currently used."""
return openpype.version.__version__
def get_pype_version():
"""Version of pype that is currently used."""
return openpype.version.__version__
"""Backwards compatibility. Remove when 100% not used."""
print((
"Using deprecated function 'openpype.lib.pype_info.get_pype_version'"
" replace with 'openpype.lib.pype_info.get_openpype_version'."
))
return get_openpype_version()
def get_build_version():
"""OpenPype version of build."""
# Return OpenPype version if is running from code
if not is_running_from_build():
return get_openpype_version()
# Import `version.py` from build directory
version_filepath = os.path.join(
os.environ["OPENPYPE_ROOT"],
"openpype",
"version.py"
)
if not os.path.exists(version_filepath):
return None
module = import_filepath(version_filepath, "openpype_build_version")
return getattr(module, "__version__", None)
def is_running_from_build():
"""Determine if current process is running from build or code.
Returns:
bool: True if running from build.
"""
executable_path = os.environ["OPENPYPE_EXECUTABLE"]
executable_filename = os.path.basename(executable_path)
if "python" in executable_filename.lower():
return False
return True
def is_running_staging():
"""Currently used OpenPype is staging version.
Returns:
bool: True if openpype version containt 'staging'.
"""
if "staging" in get_openpype_version():
return True
return False
def get_pype_info():
"""Information about currently used Pype process."""
executable_args = get_pype_execute_args()
if len(executable_args) == 1:
if is_running_from_build():
version_type = "build"
else:
version_type = "code"
return {
"version": get_pype_version(),
"version": get_openpype_version(),
"version_type": version_type,
"executable": executable_args[-1],
"pype_root": os.environ["OPENPYPE_REPOS_ROOT"],
@ -73,7 +126,7 @@ def extract_pype_info_to_file(dirpath):
filepath (str): Full path to file where data were extracted.
"""
filename = "{}_{}_{}.json".format(
get_pype_version(),
get_openpype_version(),
get_local_site_id(),
datetime.datetime.now().strftime("%y%m%d%H%M%S")
)

View file

@ -1,125 +1,143 @@
# OpenPype modules/addons
OpenPype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering or may contain only special plugins. Addons work the same way currently there is no difference in module and addon.
OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its usage code, Deadline farm rendering or may contain only special plugins. Addons work the same way currently, there is no difference between module and addon functionality.
## Modules concept
- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the modulo located
- modules or addons should never be imported directly even if you know possible full import path
- it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts
- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the module located
- modules or addons should never be imported directly, even if you know possible full import path
- it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts
### TODOs
- add module/addon manifest
- definition of module (not 100% defined content e.g. minimum require OpenPype version etc.)
- defying that folder is content of a module or an addon
- module/addon have it's settings schemas and default values outside OpenPype
- add general setting of paths to modules
- definition of module (not 100% defined content e.g. minimum required OpenPype version etc.)
- defining a folder as a content of a module or an addon
## Base class `OpenPypeModule`
- abstract class as base for each module
- implementation should be module's api withou GUI parts
- may implement `get_global_environments` method which should return dictionary of environments that are globally appliable and value is the same for whole studio if launched at any workstation (except os specific paths)
- implementation should contain module's api without GUI parts
- may implement `get_global_environments` method which should return dictionary of environments that are globally applicable and value is the same for whole studio if launched at any workstation (except os specific paths)
- abstract parts:
- `name` attribute - name of a module
- `initialize` method - method for own initialization of a module (should not override `__init__`)
- `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules
- `__init__` should not be overriden and `initialize` should not do time consuming part but only prepare base data about module
- also keep in mind that they may be initialized in headless mode
- `name` attribute - name of a module
- `initialize` method - method for own initialization of a module (should not override `__init__`)
- `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules
- `__init__` should not be overridden and `initialize` should not do time consuming part but only prepare base data about module
- also keep in mind that they may be initialized in headless mode
- connection with other modules is made with help of interfaces
## Addon class `OpenPypeAddOn`
- inherits from `OpenPypeModule` but is enabled by default and doesn't have to implement `initialize` and `connect_with_modules` methods
- that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...)
## How to add addons/modules
- in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder
- for openpype example addons use `{OPENPYPE_REPOS_ROOT}/openpype/modules/example_addons`
## Addon/module settings
- addons/modules may have defined custom settings definitions with default values
- it is based on settings type `dynamic_schema` which has `name`
- that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults
- they can't be added to any schema hierarchy
- item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`)
- addons may define it's dynamic schema items
- they can be defined with class which inherits from `BaseModuleSettingsDef`
- it is recommended to use pre implemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values
- check it's docstring and check for `example_addon` in example addons
- settings definition returns schemas by dynamic schemas names
# Interfaces
- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods
- interface is class that has defined abstract methods to implement and may contain pre implemented helper methods
- module that inherit from an interface must implement those abstract methods otherwise won't be initialized
- it is easy to find which module object inherited from which interfaces withh 100% chance they have implemented required methods
- it is easy to find which module object inherited from which interfaces with 100% chance they have implemented required methods
- interfaces can be defined in `interfaces.py` inside module directory
- the file can't use relative imports or import anything from other parts
of module itself at the header of file
- this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation
- the file can't use relative imports or import anything from other parts
of module itself at the header of file
- this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation
## Base class `OpenPypeInterface`
- has nothing implemented
- has ABCMeta as metaclass
- is defined to be able find out classes which inherit from this base to be
able tell this is an Interface
able tell this is an Interface
## Global interfaces
- few interfaces are implemented for global usage
### IPluginPaths
- module want to add directory path/s to avalon or publish plugins
- module wants to add directory path/s to avalon or publish plugins
- module must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"`
- each key may contain list or string with path to directory with plugins
- each key may contain list or string with a path to directory with plugins
### ITrayModule
- module has more logic when used in tray
- it is possible that module can be used only in tray
- module has more logic when used in a tray
- it is possible that module can be used only in the tray
- abstract methods
- `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules`
- `tray_menu` - add actions to tray widget's menu that represent the module
- `tray_start` - start of module's login in tray
- module is initialized and connected with other modules
- `tray_exit` - module's cleanup like stop and join threads etc.
- order of calling is based on implementation this order is how it works with `TrayModulesManager`
- it is recommended to import and use GUI implementaion only in these methods
- `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules`
- `tray_menu` - add actions to tray widget's menu that represent the module
- `tray_start` - start of module's login in tray
- module is initialized and connected with other modules
- `tray_exit` - module's cleanup like stop and join threads etc.
- order of calling is based on implementation this order is how it works with `TrayModulesManager`
- it is recommended to import and use GUI implementation only in these methods
- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init`
- if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations
- if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations
### ITrayService
- inherit from `ITrayModule` and implement `tray_menu` method for you
- add action to submenu "Services" in tray widget menu with icon and label
- abstract atttribute `label`
- label shown in menu
- interface has preimplemented methods to change icon color
- `set_service_running` - green icon
- `set_service_failed` - red icon
- `set_service_idle` - orange icon
- these states must be set by module itself `set_service_running` is default state on initialization
- inherits from `ITrayModule` and implements `tray_menu` method for you
- adds action to submenu "Services" in tray widget menu with icon and label
- abstract attribute `label`
- label shown in menu
- interface has pre implemented methods to change icon color
- `set_service_running` - green icon
- `set_service_failed` - red icon
- `set_service_idle` - orange icon
- these states must be set by module itself `set_service_running` is default state on initialization
### ITrayAction
- inherit from `ITrayModule` and implement `tray_menu` method for you
- add action to tray widget menu with label
- abstract atttribute `label`
- label shown in menu
- inherits from `ITrayModule` and implements `tray_menu` method for you
- adds action to tray widget menu with label
- abstract attribute `label`
- label shown in menu
- abstract method `on_action_trigger`
- what should happen when action is triggered
- NOTE: It is good idea to implement logic in `on_action_trigger` to api method and trigger that methods on callbacks this gives ability to trigger that method outside tray
- what should happen when an action is triggered
- NOTE: It is a good idea to implement logic in `on_action_trigger` to the api method and trigger that method on callbacks. This gives ability to trigger that method outside tray
## Modules interfaces
- modules may have defined their interfaces to be able recognize other modules that would want to use their features
-
### Example:
- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which of other modules want to add paths to server/user event handlers
- Clockify module use `IFtrackEventHandlerPaths` and return paths to clockify ftrack synchronizers
- modules may have defined their own interfaces to be able to recognize other modules that would want to use their features
- Clockify has more inharitance it's class definition looks like
### Example:
- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which other modules want to add paths to server/user event handlers
- Clockify module use `IFtrackEventHandlerPaths` and returns paths to clockify ftrack synchronizers
- Clockify inherits from more interfaces. It's class definition looks like:
```
class ClockifyModule(
OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize.
ITrayModule, # Says has special implementation when used in tray.
IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher).
IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server.
ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module.
OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize.
ITrayModule, # Says has special implementation when used in tray.
IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher).
IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server.
ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module.
):
```
### ModulesManager
- collect module classes and tries to initialize them
- collects module classes and tries to initialize them
- important attributes
- `modules` - list of available attributes
- `modules_by_id` - dictionary of modules mapped by their ids
- `modules_by_name` - dictionary of modules mapped by their names
- all these attributes contain all found modules even if are not enabled
- `modules` - list of available attributes
- `modules_by_id` - dictionary of modules mapped by their ids
- `modules_by_name` - dictionary of modules mapped by their names
- all these attributes contain all found modules even if are not enabled
- helper methods
- `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them
- `collect_plugin_paths` collect plugin paths from all enabled modules
- output is always dictionary with all keys and values as list
```
{
"publish": [],
"create": [],
"load": [],
"actions": []
}
```
- `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them
- `collect_plugin_paths` collects plugin paths from all enabled modules
- output is always dictionary with all keys and values as an list
```
{
"publish": [],
"create": [],
"load": [],
"actions": []
}
```
### TrayModulesManager
- inherit from `ModulesManager`
- has specific implementations for Pype Tray tool and handle `ITrayModule` methods
- inherits from `ModulesManager`
- has specific implementation for Pype Tray tool and handle `ITrayModule` methods

View file

@ -1,21 +1,35 @@
# -*- coding: utf-8 -*-
from .base import (
OpenPypeModule,
OpenPypeAddOn,
OpenPypeInterface,
load_modules,
ModulesManager,
TrayModulesManager
TrayModulesManager,
BaseModuleSettingsDef,
ModuleSettingsDef,
JsonFilesSettingsDef,
get_module_settings_defs
)
__all__ = (
"OpenPypeModule",
"OpenPypeAddOn",
"OpenPypeInterface",
"load_modules",
"ModulesManager",
"TrayModulesManager"
"TrayModulesManager",
"BaseModuleSettingsDef",
"ModuleSettingsDef",
"JsonFilesSettingsDef",
"get_module_settings_defs"
)

View file

@ -2,9 +2,11 @@
"""Base class for Pype Modules."""
import os
import sys
import json
import time
import inspect
import logging
import platform
import threading
import collections
from uuid import uuid4
@ -12,7 +14,18 @@ from abc import ABCMeta, abstractmethod
import six
import openpype
from openpype.settings import get_system_settings
from openpype.settings import (
get_system_settings,
SYSTEM_SETTINGS_KEY,
PROJECT_SETTINGS_KEY,
SCHEMA_KEY_SYSTEM_SETTINGS,
SCHEMA_KEY_PROJECT_SETTINGS
)
from openpype.settings.lib import (
get_studio_system_settings_overrides,
load_json_file
)
from openpype.lib import PypeLogger
@ -115,11 +128,51 @@ def get_default_modules_dir():
return os.path.join(current_dir, "default_modules")
def get_dynamic_modules_dirs():
"""Possible paths to OpenPype Addons of Modules.
Paths are loaded from studio settings under:
`modules -> addon_paths -> {platform name}`
Path may contain environment variable as a formatting string.
They are not validated or checked their existence.
Returns:
list: Paths loaded from studio overrides.
"""
output = []
value = get_studio_system_settings_overrides()
for key in ("modules", "addon_paths", platform.system().lower()):
if key not in value:
return output
value = value[key]
for path in value:
if not path:
continue
try:
path = path.format(**os.environ)
except Exception:
pass
output.append(path)
return output
def get_module_dirs():
"""List of paths where OpenPype modules can be found."""
dirpaths = [
get_default_modules_dir()
]
_dirpaths = []
_dirpaths.append(get_default_modules_dir())
_dirpaths.extend(get_dynamic_modules_dirs())
dirpaths = []
for path in _dirpaths:
if not path:
continue
normalized = os.path.normpath(path)
if normalized not in dirpaths:
dirpaths.append(normalized)
return dirpaths
@ -165,6 +218,9 @@ def _load_interfaces():
os.path.join(get_default_modules_dir(), "interfaces.py")
)
for dirpath in dirpaths:
if not os.path.exists(dirpath):
continue
for filename in os.listdir(dirpath):
if filename in ("__pycache__", ):
continue
@ -272,12 +328,19 @@ def _load_modules():
# TODO add more logic how to define if folder is module or not
# - check manifest and content of manifest
if os.path.isdir(fullpath):
import_module_from_dirpath(dirpath, filename, modules_key)
try:
if os.path.isdir(fullpath):
import_module_from_dirpath(dirpath, filename, modules_key)
elif ext in (".py", ):
module = import_filepath(fullpath)
setattr(openpype_modules, basename, module)
elif ext in (".py", ):
module = import_filepath(fullpath)
setattr(openpype_modules, basename, module)
except Exception:
log.error(
"Failed to import '{}'.".format(fullpath),
exc_info=True
)
class _OpenPypeInterfaceMeta(ABCMeta):
@ -354,7 +417,6 @@ class OpenPypeModule:
"""
pass
@abstractmethod
def connect_with_modules(self, enabled_modules):
"""Connect with other enabled modules."""
pass
@ -368,7 +430,12 @@ class OpenPypeModule:
class OpenPypeAddOn(OpenPypeModule):
pass
# Enable Addon by default
enabled = True
def initialize(self, module_settings):
"""Initialization is not be required for most of addons."""
pass
class ModulesManager:
@ -423,6 +490,7 @@ class ModulesManager:
if (
not inspect.isclass(modules_item)
or modules_item is OpenPypeModule
or modules_item is OpenPypeAddOn
or not issubclass(modules_item, OpenPypeModule)
):
continue
@ -920,3 +988,424 @@ class TrayModulesManager(ModulesManager):
),
exc_info=True
)
def get_module_settings_defs():
"""Check loaded addons/modules for existence of thei settings definition.
Check if OpenPype addon/module as python module has class that inherit
from `ModuleSettingsDef` in python module variables (imported
in `__init__py`).
Returns:
list: All valid and not abstract settings definitions from imported
openpype addons and modules.
"""
# Make sure modules are loaded
load_modules()
import openpype_modules
settings_defs = []
log = PypeLogger.get_logger("ModuleSettingsLoad")
for raw_module in openpype_modules:
for attr_name in dir(raw_module):
attr = getattr(raw_module, attr_name)
if (
not inspect.isclass(attr)
or attr is ModuleSettingsDef
or not issubclass(attr, ModuleSettingsDef)
):
continue
if inspect.isabstract(attr):
# Find missing implementations by convetion on `abc` module
not_implemented = []
for attr_name in dir(attr):
attr = getattr(attr, attr_name, None)
abs_method = getattr(
attr, "__isabstractmethod__", None
)
if attr and abs_method:
not_implemented.append(attr_name)
# Log missing implementations
log.warning((
"Skipping abstract Class: {} in module {}."
" Missing implementations: {}"
).format(
attr_name, raw_module.__name__, ", ".join(not_implemented)
))
continue
settings_defs.append(attr)
return settings_defs
@six.add_metaclass(ABCMeta)
class BaseModuleSettingsDef:
"""Definition of settings for OpenPype module or AddOn."""
_id = None
@property
def id(self):
"""ID created on initialization.
ID should be per created object. Helps to store objects.
"""
if self._id is None:
self._id = uuid4()
return self._id
@abstractmethod
def get_settings_schemas(self, schema_type):
"""Setting schemas for passed schema type.
These are main schemas by dynamic schema keys. If they're using
sub schemas or templates they should be loaded with
`get_dynamic_schemas`.
Returns:
dict: Schema by `dynamic_schema` keys.
"""
pass
@abstractmethod
def get_dynamic_schemas(self, schema_type):
"""Settings schemas and templates that can be used anywhere.
It is recommended to add prefix specific for addon/module to keys
(e.g. "my_addon/real_schema_name").
Returns:
dict: Schemas and templates by their keys.
"""
pass
@abstractmethod
def get_defaults(self, top_key):
"""Default values for passed top key.
Top keys are (currently) "system_settings" or "project_settings".
Should return exactly what was passed with `save_defaults`.
Returns:
dict: Default values by path to first key in OpenPype defaults.
"""
pass
@abstractmethod
def save_defaults(self, top_key, data):
"""Save default values for passed top key.
Top keys are (currently) "system_settings" or "project_settings".
Passed data are by path to first key defined in main schemas.
"""
pass
class ModuleSettingsDef(BaseModuleSettingsDef):
"""Settings definiton with separated system and procect settings parts.
Reduce conditions that must be checked and adds predefined methods for
each case.
"""
def get_defaults(self, top_key):
"""Split method into 2 methods by top key."""
if top_key == SYSTEM_SETTINGS_KEY:
return self.get_default_system_settings() or {}
elif top_key == PROJECT_SETTINGS_KEY:
return self.get_default_project_settings() or {}
return {}
def save_defaults(self, top_key, data):
"""Split method into 2 methods by top key."""
if top_key == SYSTEM_SETTINGS_KEY:
self.save_system_defaults(data)
elif top_key == PROJECT_SETTINGS_KEY:
self.save_project_defaults(data)
def get_settings_schemas(self, schema_type):
"""Split method into 2 methods by schema type."""
if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS:
return self.get_system_settings_schemas() or {}
elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS:
return self.get_project_settings_schemas() or {}
return {}
def get_dynamic_schemas(self, schema_type):
"""Split method into 2 methods by schema type."""
if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS:
return self.get_system_dynamic_schemas() or {}
elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS:
return self.get_project_dynamic_schemas() or {}
return {}
@abstractmethod
def get_system_settings_schemas(self):
"""Schemas and templates usable in system settings schemas.
Returns:
dict: Schemas and templates by it's names. Names must be unique
across whole OpenPype.
"""
pass
@abstractmethod
def get_project_settings_schemas(self):
"""Schemas and templates usable in project settings schemas.
Returns:
dict: Schemas and templates by it's names. Names must be unique
across whole OpenPype.
"""
pass
@abstractmethod
def get_system_dynamic_schemas(self):
"""System schemas by dynamic schema name.
If dynamic schema name is not available in then schema will not used.
Returns:
dict: Schemas or list of schemas by dynamic schema name.
"""
pass
@abstractmethod
def get_project_dynamic_schemas(self):
"""Project schemas by dynamic schema name.
If dynamic schema name is not available in then schema will not used.
Returns:
dict: Schemas or list of schemas by dynamic schema name.
"""
pass
@abstractmethod
def get_default_system_settings(self):
"""Default system settings values.
Returns:
dict: Default values by path to first key.
"""
pass
@abstractmethod
def get_default_project_settings(self):
"""Default project settings values.
Returns:
dict: Default values by path to first key.
"""
pass
@abstractmethod
def save_system_defaults(self, data):
"""Save default system settings values.
Passed data are by path to first key defined in main schemas.
"""
pass
@abstractmethod
def save_project_defaults(self, data):
"""Save default project settings values.
Passed data are by path to first key defined in main schemas.
"""
pass
class JsonFilesSettingsDef(ModuleSettingsDef):
"""Preimplemented settings definition using json files and file structure.
Expected file structure:
root
# Default values
defaults
system_settings.json
project_settings.json
# Schemas for `dynamic_template` type
dynamic_schemas
system_dynamic_schemas.json
project_dynamic_schemas.json
# Schemas that can be used anywhere (enhancement for `dynamic_schemas`)
schemas
system_schemas
<system schema.json> # Any schema or template files
...
project_schemas
<system schema.json> # Any schema or template files
...
Schemas can be loaded with prefix to avoid duplicated schema/template names
across all OpenPype addons/modules. Prefix can be defined with class
attribute `schema_prefix`.
Only think which must be implemented in `get_settings_root_path` which
should return directory path to `root` (in structure graph above).
"""
# Possible way how to define `schemas` prefix
schema_prefix = ""
@abstractmethod
def get_settings_root_path(self):
"""Directory path where settings and it's schemas are located."""
pass
def __init__(self):
settings_root_dir = self.get_settings_root_path()
defaults_dir = os.path.join(
settings_root_dir, "defaults"
)
dynamic_schemas_dir = os.path.join(
settings_root_dir, "dynamic_schemas"
)
schemas_dir = os.path.join(
settings_root_dir, "schemas"
)
self.system_defaults_filepath = os.path.join(
defaults_dir, "system_settings.json"
)
self.project_defaults_filepath = os.path.join(
defaults_dir, "project_settings.json"
)
self.system_dynamic_schemas_filepath = os.path.join(
dynamic_schemas_dir, "system_dynamic_schemas.json"
)
self.project_dynamic_schemas_filepath = os.path.join(
dynamic_schemas_dir, "project_dynamic_schemas.json"
)
self.system_schemas_dir = os.path.join(
schemas_dir, "system_schemas"
)
self.project_schemas_dir = os.path.join(
schemas_dir, "project_schemas"
)
def _load_json_file_data(self, path):
if os.path.exists(path):
return load_json_file(path)
return {}
def get_default_system_settings(self):
"""Default system settings values.
Returns:
dict: Default values by path to first key.
"""
return self._load_json_file_data(self.system_defaults_filepath)
def get_default_project_settings(self):
"""Default project settings values.
Returns:
dict: Default values by path to first key.
"""
return self._load_json_file_data(self.project_defaults_filepath)
def _save_data_to_filepath(self, path, data):
dirpath = os.path.dirname(path)
if not os.path.exists(dirpath):
os.makedirs(dirpath)
with open(path, "w") as file_stream:
json.dump(data, file_stream, indent=4)
def save_system_defaults(self, data):
"""Save default system settings values.
Passed data are by path to first key defined in main schemas.
"""
self._save_data_to_filepath(self.system_defaults_filepath, data)
def save_project_defaults(self, data):
"""Save default project settings values.
Passed data are by path to first key defined in main schemas.
"""
self._save_data_to_filepath(self.project_defaults_filepath, data)
def get_system_dynamic_schemas(self):
"""System schemas by dynamic schema name.
If dynamic schema name is not available in then schema will not used.
Returns:
dict: Schemas or list of schemas by dynamic schema name.
"""
return self._load_json_file_data(self.system_dynamic_schemas_filepath)
def get_project_dynamic_schemas(self):
"""Project schemas by dynamic schema name.
If dynamic schema name is not available in then schema will not used.
Returns:
dict: Schemas or list of schemas by dynamic schema name.
"""
return self._load_json_file_data(self.project_dynamic_schemas_filepath)
def _load_files_from_path(self, path):
output = {}
if not path or not os.path.exists(path):
return output
if os.path.isfile(path):
filename = os.path.basename(path)
basename, ext = os.path.splitext(filename)
if ext == ".json":
if self.schema_prefix:
key = "{}/{}".format(self.schema_prefix, basename)
else:
key = basename
output[key] = self._load_json_file_data(path)
return output
path = os.path.normpath(path)
for root, _, files in os.walk(path, topdown=False):
for filename in files:
basename, ext = os.path.splitext(filename)
if ext != ".json":
continue
json_path = os.path.join(root, filename)
store_key = os.path.join(
root.replace(path, ""), basename
).replace("\\", "/")
if self.schema_prefix:
store_key = "{}/{}".format(self.schema_prefix, store_key)
output[store_key] = self._load_json_file_data(json_path)
return output
def get_system_settings_schemas(self):
"""Schemas and templates usable in system settings schemas.
Returns:
dict: Schemas and templates by it's names. Names must be unique
across whole OpenPype.
"""
return self._load_files_from_path(self.system_schemas_dir)
def get_project_settings_schemas(self):
"""Schemas and templates usable in project settings schemas.
Returns:
dict: Schemas and templates by it's names. Names must be unique
across whole OpenPype.
"""
return self._load_files_from_path(self.project_schemas_dir)

View file

@ -2,13 +2,10 @@ import os
import openpype
from openpype import resources
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IWebServerRoutes
)
from openpype_interfaces import ITrayModule
class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
class AvalonModule(OpenPypeModule, ITrayModule):
name = "avalon"
def initialize(self, modules_settings):
@ -55,12 +52,12 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
def tray_init(self):
# Add library tool
try:
from avalon.tools.libraryloader import app
from avalon import style
from Qt import QtGui
from avalon import style
from openpype.tools.libraryloader import LibraryLoaderWindow
self.libraryloader = app.Window(
icon=QtGui.QIcon(resources.pype_icon_filepath()),
self.libraryloader = LibraryLoaderWindow(
icon=QtGui.QIcon(resources.get_openpype_icon_filepath()),
show_projects=True,
show_libraries=True
)
@ -71,16 +68,6 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
exc_info=True
)
def connect_with_modules(self, _enabled_modules):
return
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import AvalonRestApiResource
self.rest_api_obj = AvalonRestApiResource(self, server_manager)
# Definition of Tray menu
def tray_menu(self, tray_menu):
from Qt import QtWidgets
@ -108,3 +95,10 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
# for Windows
self.libraryloader.activateWindow()
self.libraryloader.refresh()
# Webserver module implementation
def webserver_initialization(self, server_manager):
"""Add routes for webserver."""
if self.tray_initialized:
from .rest_api import AvalonRestApiResource
self.rest_api_obj = AvalonRestApiResource(self, server_manager)

View file

@ -10,18 +10,14 @@ from .constants import (
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IPluginPaths,
IFtrackEventHandlerPaths,
ITimersManager
IPluginPaths
)
class ClockifyModule(
OpenPypeModule,
ITrayModule,
IPluginPaths,
IFtrackEventHandlerPaths,
ITimersManager
IPluginPaths
):
name = "clockify"
@ -39,6 +35,11 @@ class ClockifyModule(
self.clockapi = ClockifyAPI(master_parent=self)
# TimersManager attributes
# - set `timers_manager_connector` only in `tray_init`
self.timers_manager_connector = None
self._timers_manager_module = None
def get_global_environments(self):
return {
"CLOCKIFY_WORKSPACE": self.workspace_name
@ -61,6 +62,9 @@ class ClockifyModule(
self.bool_timer_run = False
self.bool_api_key_set = self.clockapi.set_api()
# Define itself as TimersManager connector
self.timers_manager_connector = self
def tray_start(self):
if self.bool_api_key_set is False:
self.show_settings()
@ -87,16 +91,13 @@ class ClockifyModule(
"actions": [actions_path]
}
def get_event_handler_paths(self):
"""Implementaton of IFtrackEventHandlerPaths to get plugin paths."""
def get_ftrack_event_handler_paths(self):
"""Function for Ftrack module to add ftrack event handler paths."""
return {
"user": [CLOCKIFY_FTRACK_USER_PATH],
"server": [CLOCKIFY_FTRACK_SERVER_PATH]
}
def connect_with_modules(self, *_a, **_kw):
return
def clockify_timer_stopped(self):
self.bool_timer_run = False
# Call `ITimersManager` method
@ -165,10 +166,6 @@ class ClockifyModule(
self.set_menu_visibility()
time.sleep(5)
def stop_timer(self):
"""Implementation of ITimersManager."""
self.clockapi.finish_time_entry()
def signed_in(self):
if not self.timer_manager:
return
@ -179,8 +176,60 @@ class ClockifyModule(
if self.timer_manager.is_running:
self.start_timer_manager(self.timer_manager.last_task)
def on_message_widget_close(self):
self.message_widget = None
# Definition of Tray menu
def tray_menu(self, parent_menu):
# Menu for Tray App
from Qt import QtWidgets
menu = QtWidgets.QMenu("Clockify", parent_menu)
menu.setProperty("submenu", "on")
# Actions
action_show_settings = QtWidgets.QAction("Settings", menu)
action_stop_timer = QtWidgets.QAction("Stop timer", menu)
menu.addAction(action_show_settings)
menu.addAction(action_stop_timer)
action_show_settings.triggered.connect(self.show_settings)
action_stop_timer.triggered.connect(self.stop_timer)
self.action_stop_timer = action_stop_timer
self.set_menu_visibility()
parent_menu.addMenu(menu)
def show_settings(self):
self.widget_settings.input_api_key.setText(self.clockapi.get_api_key())
self.widget_settings.show()
def set_menu_visibility(self):
self.action_stop_timer.setVisible(self.bool_timer_run)
# --- TimersManager connection methods ---
def register_timers_manager(self, timer_manager_module):
"""Store TimersManager for future use."""
self._timers_manager_module = timer_manager_module
def timer_started(self, data):
"""Tell TimersManager that timer started."""
if self._timers_manager_module is not None:
self._timers_manager_module.timer_started(self._module.id, data)
def timer_stopped(self):
"""Tell TimersManager that timer stopped."""
if self._timers_manager_module is not None:
self._timers_manager_module.timer_stopped(self._module.id)
def stop_timer(self):
"""Called from TimersManager to stop timer."""
self.clockapi.finish_time_entry()
def start_timer(self, input_data):
"""Implementation of ITimersManager."""
"""Called from TimersManager to start timer."""
# If not api key is not entered then skip
if not self.clockapi.get_api_key():
return
@ -237,36 +286,3 @@ class ClockifyModule(
self.clockapi.start_time_entry(
description, project_id, tag_ids=tag_ids
)
def on_message_widget_close(self):
self.message_widget = None
# Definition of Tray menu
def tray_menu(self, parent_menu):
# Menu for Tray App
from Qt import QtWidgets
menu = QtWidgets.QMenu("Clockify", parent_menu)
menu.setProperty("submenu", "on")
# Actions
action_show_settings = QtWidgets.QAction("Settings", menu)
action_stop_timer = QtWidgets.QAction("Stop timer", menu)
menu.addAction(action_show_settings)
menu.addAction(action_stop_timer)
action_show_settings.triggered.connect(self.show_settings)
action_stop_timer.triggered.connect(self.stop_timer)
self.action_stop_timer = action_stop_timer
self.set_menu_visibility()
parent_menu.addMenu(menu)
def show_settings(self):
self.widget_settings.input_api_key.setText(self.clockapi.get_api_key())
self.widget_settings.show()
def set_menu_visibility(self):
self.action_stop_timer.setVisible(self.bool_timer_run)

View file

@ -13,7 +13,7 @@ class MessageWidget(QtWidgets.QWidget):
super(MessageWidget, self).__init__()
# Icon
icon = QtGui.QIcon(resources.pype_icon_filepath())
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(
@ -90,7 +90,7 @@ class ClockifySettings(QtWidgets.QWidget):
self.validated = False
# Icon
icon = QtGui.QIcon(resources.pype_icon_filepath())
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowTitle("Clockify settings")

View file

@ -26,9 +26,6 @@ class DeadlineModule(OpenPypeModule, IPluginPaths):
"not specified. Disabling module."))
return
def connect_with_modules(self, *_a, **_kw):
return
def get_plugin_paths(self):
"""Deadline plugin paths."""
current_dir = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,4 +1,6 @@
import time
import sys
import json
import traceback
from openpype_modules.ftrack.lib import ServerAction
@ -52,17 +54,80 @@ class SyncToAvalonServer(ServerAction):
return False
def launch(self, session, in_entities, event):
self.log.debug("{}: Creating job".format(self.label))
user_entity = session.query(
"User where id is {}".format(event["source"]["user"]["id"])
).one()
job_entity = session.create("Job", {
"user": user_entity,
"status": "running",
"data": json.dumps({
"description": "Sync to avalon is running..."
})
})
session.commit()
project_entity = self.get_project_from_entity(in_entities[0])
project_name = project_entity["full_name"]
try:
result = self.synchronization(event, project_name)
except Exception:
self.log.error(
"Synchronization failed due to code error", exc_info=True
)
description = "Sync to avalon Crashed (Download traceback)"
self.add_traceback_to_job(
job_entity, session, sys.exc_info(), description
)
msg = "An error has happened during synchronization"
title = "Synchronization report ({}):".format(project_name)
items = []
items.append({
"type": "label",
"value": "# {}".format(msg)
})
items.append({
"type": "label",
"value": (
"<p>Download report from job for more information.</p>"
)
})
report = {}
try:
report = self.entities_factory.report()
except Exception:
pass
_items = report.get("items") or []
if _items:
items.append(self.entities_factory.report_splitter)
items.extend(_items)
self.show_interface(items, title, event, submit_btn_label="Ok")
return {"success": True, "message": msg}
job_entity["status"] = "done"
job_entity["data"] = json.dumps({
"description": "Sync to avalon finished."
})
session.commit()
return result
def synchronization(self, event, project_name):
time_start = time.time()
self.show_message(event, "Synchronization - Preparing data", True)
# Get ftrack project
if in_entities[0].entity_type.lower() == "project":
ft_project_name = in_entities[0]["full_name"]
else:
ft_project_name = in_entities[0]["project"]["full_name"]
try:
output = self.entities_factory.launch_setup(ft_project_name)
output = self.entities_factory.launch_setup(project_name)
if output is not None:
return output
@ -72,7 +137,7 @@ class SyncToAvalonServer(ServerAction):
time_2 = time.time()
# This must happen before all filtering!!!
self.entities_factory.prepare_avalon_entities(ft_project_name)
self.entities_factory.prepare_avalon_entities(project_name)
time_3 = time.time()
self.entities_factory.filter_by_ignore_sync()
@ -118,7 +183,7 @@ class SyncToAvalonServer(ServerAction):
report = self.entities_factory.report()
if report and report.get("items"):
default_title = "Synchronization report ({}):".format(
ft_project_name
project_name
)
self.show_interface(
items=report["items"],
@ -130,46 +195,6 @@ class SyncToAvalonServer(ServerAction):
"message": "Synchronization Finished"
}
except Exception:
self.log.error(
"Synchronization failed due to code error", exc_info=True
)
msg = "An error has happened during synchronization"
title = "Synchronization report ({}):".format(ft_project_name)
items = []
items.append({
"type": "label",
"value": "# {}".format(msg)
})
items.append({
"type": "label",
"value": "## Traceback of the error"
})
items.append({
"type": "label",
"value": "<p>{}</p>".format(
str(traceback.format_exc()).replace(
"\n", "<br>").replace(
" ", "&nbsp;"
)
)
})
report = {"items": []}
try:
report = self.entities_factory.report()
except Exception:
pass
_items = report.get("items", [])
if _items:
items.append(self.entities_factory.report_splitter)
items.extend(_items)
self.show_interface(items, title, event)
return {"success": True, "message": msg}
finally:
try:
self.entities_factory.dbcon.uninstall()

View file

@ -3,7 +3,7 @@ import re
import json
from openpype_modules.ftrack.lib import BaseAction, statics_icon
from openpype.api import Anatomy, get_project_settings
from openpype.api import get_project_basic_paths, create_project_folders
class CreateProjectFolders(BaseAction):
@ -72,25 +72,18 @@ class CreateProjectFolders(BaseAction):
def launch(self, session, entities, event):
# Get project entity
project_entity = self.get_project_from_entity(entities[0])
# Load settings for project
project_name = project_entity["full_name"]
project_settings = get_project_settings(project_name)
project_folder_structure = (
project_settings["global"]["project_folder_structure"]
)
if not project_folder_structure:
return {
"success": False,
"message": "Project structure is not set."
}
try:
if isinstance(project_folder_structure, str):
project_folder_structure = json.loads(project_folder_structure)
# Get paths based on presets
basic_paths = self.get_path_items(project_folder_structure)
self.create_folders(basic_paths, project_entity)
basic_paths = get_project_basic_paths(project_name)
if not basic_paths:
return {
"success": False,
"message": "Project structure is not set."
}
# Invoking OpenPype API to create the project folders
create_project_folders(basic_paths, project_name)
self.create_ftrack_entities(basic_paths, project_entity)
except Exception as exc:
@ -195,58 +188,6 @@ class CreateProjectFolders(BaseAction):
self.session.commit()
return new_ent
def get_path_items(self, in_dict):
output = []
for key, value in in_dict.items():
if not value:
output.append(key)
else:
paths = self.get_path_items(value)
for path in paths:
if not isinstance(path, (list, tuple)):
path = [path]
output.append([key, *path])
return output
def compute_paths(self, basic_paths_items, project_root):
output = []
for path_items in basic_paths_items:
clean_items = []
for path_item in path_items:
matches = re.findall(self.pattern_array, path_item)
if len(matches) > 0:
path_item = path_item.replace(matches[0], "")
if path_item == self.project_root_key:
path_item = project_root
clean_items.append(path_item)
output.append(os.path.normpath(os.path.sep.join(clean_items)))
return output
def create_folders(self, basic_paths, project):
anatomy = Anatomy(project["full_name"])
roots_paths = []
if isinstance(anatomy.roots, dict):
for root in anatomy.roots.values():
roots_paths.append(root.value)
else:
roots_paths.append(anatomy.roots.value)
for root_path in roots_paths:
project_root = os.path.join(root_path, project["full_name"])
full_paths = self.compute_paths(basic_paths, project_root)
# Create folders
for path in full_paths:
full_path = path.format(project_root=project_root)
if os.path.exists(full_path):
self.log.debug(
"Folder already exists: {}".format(full_path)
)
else:
self.log.debug("Creating folder: {}".format(full_path))
os.makedirs(full_path)
def register(session):
CreateProjectFolders(session).register()

View file

@ -23,6 +23,8 @@ class DeleteOldVersions(BaseAction):
)
icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg")
settings_key = "delete_old_versions"
dbcon = AvalonMongoDB()
inteface_title = "Choose your preferences"

View file

@ -1,4 +1,6 @@
import time
import sys
import json
import traceback
from openpype_modules.ftrack.lib import BaseAction, statics_icon
@ -30,17 +32,10 @@ class SyncToAvalonLocal(BaseAction):
- or do it manually (Not recommended)
"""
#: Action identifier.
identifier = "sync.to.avalon.local"
#: Action label.
label = "OpenPype Admin"
#: Action variant
variant = "- Sync To Avalon (Local)"
#: Action description.
description = "Send data from Ftrack to Avalon"
#: priority
priority = 200
#: roles that are allowed to register this action
icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg")
settings_key = "sync_to_avalon_local"
@ -63,17 +58,80 @@ class SyncToAvalonLocal(BaseAction):
return is_valid
def launch(self, session, in_entities, event):
self.log.debug("{}: Creating job".format(self.label))
user_entity = session.query(
"User where id is {}".format(event["source"]["user"]["id"])
).one()
job_entity = session.create("Job", {
"user": user_entity,
"status": "running",
"data": json.dumps({
"description": "Sync to avalon is running..."
})
})
session.commit()
project_entity = self.get_project_from_entity(in_entities[0])
project_name = project_entity["full_name"]
try:
result = self.synchronization(event, project_name)
except Exception:
self.log.error(
"Synchronization failed due to code error", exc_info=True
)
description = "Sync to avalon Crashed (Download traceback)"
self.add_traceback_to_job(
job_entity, session, sys.exc_info(), description
)
msg = "An error has happened during synchronization"
title = "Synchronization report ({}):".format(project_name)
items = []
items.append({
"type": "label",
"value": "# {}".format(msg)
})
items.append({
"type": "label",
"value": (
"<p>Download report from job for more information.</p>"
)
})
report = {}
try:
report = self.entities_factory.report()
except Exception:
pass
_items = report.get("items") or []
if _items:
items.append(self.entities_factory.report_splitter)
items.extend(_items)
self.show_interface(items, title, event, submit_btn_label="Ok")
return {"success": True, "message": msg}
job_entity["status"] = "done"
job_entity["data"] = json.dumps({
"description": "Sync to avalon finished."
})
session.commit()
return result
def synchronization(self, event, project_name):
time_start = time.time()
self.show_message(event, "Synchronization - Preparing data", True)
# Get ftrack project
if in_entities[0].entity_type.lower() == "project":
ft_project_name = in_entities[0]["full_name"]
else:
ft_project_name = in_entities[0]["project"]["full_name"]
try:
output = self.entities_factory.launch_setup(ft_project_name)
output = self.entities_factory.launch_setup(project_name)
if output is not None:
return output
@ -83,7 +141,7 @@ class SyncToAvalonLocal(BaseAction):
time_2 = time.time()
# This must happen before all filtering!!!
self.entities_factory.prepare_avalon_entities(ft_project_name)
self.entities_factory.prepare_avalon_entities(project_name)
time_3 = time.time()
self.entities_factory.filter_by_ignore_sync()
@ -129,7 +187,7 @@ class SyncToAvalonLocal(BaseAction):
report = self.entities_factory.report()
if report and report.get("items"):
default_title = "Synchronization report ({}):".format(
ft_project_name
project_name
)
self.show_interface(
items=report["items"],
@ -141,46 +199,6 @@ class SyncToAvalonLocal(BaseAction):
"message": "Synchronization Finished"
}
except Exception:
self.log.error(
"Synchronization failed due to code error", exc_info=True
)
msg = "An error occurred during synchronization"
title = "Synchronization report ({}):".format(ft_project_name)
items = []
items.append({
"type": "label",
"value": "# {}".format(msg)
})
items.append({
"type": "label",
"value": "## Traceback of the error"
})
items.append({
"type": "label",
"value": "<p>{}</p>".format(
str(traceback.format_exc()).replace(
"\n", "<br>").replace(
" ", "&nbsp;"
)
)
})
report = {"items": []}
try:
report = self.entities_factory.report()
except Exception:
pass
_items = report.get("items", [])
if _items:
items.append(self.entities_factory.report_splitter)
items.extend(_items)
self.show_interface(items, title, event)
return {"success": True, "message": msg}
finally:
try:
self.entities_factory.dbcon.uninstall()

View file

@ -7,10 +7,8 @@ from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IPluginPaths,
ITimersManager,
ILaunchHookPaths,
ISettingsChangeListener,
IFtrackEventHandlerPaths
ISettingsChangeListener
)
from openpype.settings import SaveWarningExc
@ -21,7 +19,6 @@ class FtrackModule(
OpenPypeModule,
ITrayModule,
IPluginPaths,
ITimersManager,
ILaunchHookPaths,
ISettingsChangeListener
):
@ -61,6 +58,10 @@ class FtrackModule(
self.user_event_handlers_paths = user_event_handlers_paths
self.tray_module = None
# TimersManager connection
self.timers_manager_connector = None
self._timers_manager_module = None
def get_global_environments(self):
"""Ftrack's global environments."""
return {
@ -79,9 +80,17 @@ class FtrackModule(
def connect_with_modules(self, enabled_modules):
for module in enabled_modules:
if not isinstance(module, IFtrackEventHandlerPaths):
if not hasattr(module, "get_ftrack_event_handler_paths"):
continue
paths_by_type = module.get_event_handler_paths() or {}
try:
paths_by_type = module.get_ftrack_event_handler_paths()
except Exception:
continue
if not isinstance(paths_by_type, dict):
continue
for key, value in paths_by_type.items():
if not value:
continue
@ -102,16 +111,6 @@ class FtrackModule(
elif key == "user":
self.user_event_handlers_paths.extend(value)
def start_timer(self, data):
"""Implementation of ITimersManager interface."""
if self.tray_module:
self.tray_module.start_timer_manager(data)
def stop_timer(self):
"""Implementation of ITimersManager interface."""
if self.tray_module:
self.tray_module.stop_timer_manager()
def on_system_settings_save(
self, old_value, new_value, changes, new_value_metadata
):
@ -343,7 +342,10 @@ class FtrackModule(
def tray_init(self):
from .tray import FtrackTrayWrapper
self.tray_module = FtrackTrayWrapper(self)
# Module is it's own connector to TimersManager
self.timers_manager_connector = self
def tray_menu(self, parent_menu):
return self.tray_module.tray_menu(parent_menu)
@ -357,3 +359,23 @@ class FtrackModule(
def set_credentials_to_env(self, username, api_key):
os.environ["FTRACK_API_USER"] = username or ""
os.environ["FTRACK_API_KEY"] = api_key or ""
# --- TimersManager connection methods ---
def start_timer(self, data):
if self.tray_module:
self.tray_module.start_timer_manager(data)
def stop_timer(self):
if self.tray_module:
self.tray_module.stop_timer_manager()
def register_timers_manager(self, timer_manager_module):
self._timers_manager_module = timer_manager_module
def timer_started(self, data):
if self._timers_manager_module is not None:
self._timers_manager_module.timer_started(self.id, data)
def timer_stopped(self):
if self._timers_manager_module is not None:
self._timers_manager_module.timer_stopped(self.id)

View file

@ -6,7 +6,6 @@ import subprocess
import socket
import json
import platform
import argparse
import getpass
import atexit
import time
@ -16,7 +15,9 @@ import ftrack_api
import pymongo
from openpype.lib import (
get_pype_execute_args,
OpenPypeMongoConnection
OpenPypeMongoConnection,
get_openpype_version,
get_build_version
)
from openpype_modules.ftrack import FTRACK_MODULE_DIR
from openpype_modules.ftrack.lib import credentials
@ -236,14 +237,16 @@ def main_loop(ftrack_url):
statuser_thread=statuser_thread
)
system_name, pc_name = platform.uname()[:2]
host_name = socket.gethostname()
main_info = {
"created_at": datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S"),
"Username": getpass.getuser(),
"Host Name": host_name,
"Host IP": socket.gethostbyname(host_name)
}
main_info = [
["created_at", datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S")],
["Username", getpass.getuser()],
["Host Name", host_name],
["Host IP", socket.gethostbyname(host_name)],
["OpenPype executable", get_pype_execute_args()[-1]],
["OpenPype version", get_openpype_version() or "N/A"],
["OpenPype build version", get_build_version() or "N/A"]
]
main_info_str = json.dumps(main_info)
# Main loop
while True:

View file

@ -1,12 +0,0 @@
from abc import abstractmethod
from openpype.modules import OpenPypeInterface
class IFtrackEventHandlerPaths(OpenPypeInterface):
"""Other modules interface to return paths to ftrack event handlers.
Expected output is dictionary with "server" and "user" keys.
"""
@abstractmethod
def get_event_handler_paths(self):
pass

View file

@ -5,8 +5,7 @@ from .constants import (
CUST_ATTR_TOOLS,
CUST_ATTR_APPLICATIONS
)
from . settings import (
get_ftrack_url_from_settings,
from .settings import (
get_ftrack_event_mongo_info
)
from .custom_attributes import (
@ -31,7 +30,6 @@ __all__ = (
"CUST_ATTR_TOOLS",
"CUST_ATTR_APPLICATIONS",
"get_ftrack_url_from_settings",
"get_ftrack_event_mongo_info",
"default_custom_attributes_definition",

View file

@ -384,8 +384,8 @@ class BaseHandler(object):
)
def show_interface(
self, items, title='',
event=None, user=None, username=None, user_id=None
self, items, title="", event=None, user=None,
username=None, user_id=None, submit_btn_label=None
):
"""
Shows interface to user
@ -428,14 +428,18 @@ class BaseHandler(object):
'applicationId=ftrack.client.web and user.id="{0}"'
).format(user_id)
event_data = {
"type": "widget",
"items": items,
"title": title
}
if submit_btn_label:
event_data["submit_button_label"] = submit_btn_label
self.session.event_hub.publish(
ftrack_api.event.base.Event(
topic='ftrack.action.trigger-user-interface',
data=dict(
type='widget',
items=items,
title=title
),
data=event_data,
target=target
),
on_error='ignore'
@ -443,7 +447,7 @@ class BaseHandler(object):
def show_interface_from_dict(
self, messages, title="", event=None,
user=None, username=None, user_id=None
user=None, username=None, user_id=None, submit_btn_label=None
):
if not messages:
self.log.debug("No messages to show! (messages dict is empty)")
@ -469,7 +473,9 @@ class BaseHandler(object):
message = {'type': 'label', 'value': '<p>{}</p>'.format(value)}
items.append(message)
self.show_interface(items, title, event, user, username, user_id)
self.show_interface(
items, title, event, user, username, user_id, submit_btn_label
)
def trigger_action(
self, action_name, event=None, session=None,

View file

@ -1,13 +1,4 @@
import os
from openpype.api import get_system_settings
def get_ftrack_settings():
return get_system_settings()["modules"]["ftrack"]
def get_ftrack_url_from_settings():
return get_ftrack_settings()["ftrack_server"]
def get_ftrack_event_mongo_info():

View file

@ -68,9 +68,6 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin):
instance.data["families"].append("ftrack")
else:
instance.data["families"] = ["ftrack"]
else:
self.log.debug("Instance '{}' doesn't match any profile".format(
instance.data.get("family")))
def _get_add_ftrack_f_from_addit_filters(self,
additional_filters,

View file

@ -13,6 +13,11 @@ from openpype_modules.ftrack.ftrack_server.lib import (
from openpype.modules import ModulesManager
from openpype.api import Logger
from openpype.lib import (
get_openpype_version,
get_build_version
)
import ftrack_api
@ -40,9 +45,11 @@ def send_status(event):
new_event_data = {
"subprocess_id": subprocess_id,
"source": "processor",
"status_info": {
"created_at": subprocess_started.strftime("%Y.%m.%d %H:%M:%S")
}
"status_info": [
["created_at", subprocess_started.strftime("%Y.%m.%d %H:%M:%S")],
["OpenPype version", get_openpype_version() or "N/A"],
["OpenPype build version", get_build_version() or "N/A"]
]
}
new_event = ftrack_api.event.base.Event(

View file

@ -2,6 +2,7 @@ import os
import sys
import json
import threading
import collections
import signal
import socket
import datetime
@ -165,7 +166,7 @@ class StatusFactory:
return
source = event["data"]["source"]
data = event["data"]["status_info"]
data = collections.OrderedDict(event["data"]["status_info"])
self.update_status_info(source, data)
@ -348,7 +349,7 @@ def heartbeat():
def main(args):
port = int(args[-1])
server_info = json.loads(args[-2])
server_info = collections.OrderedDict(json.loads(args[-2]))
# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

View file

@ -14,7 +14,11 @@ from openpype_modules.ftrack.ftrack_server.lib import (
TOPIC_STATUS_SERVER_RESULT
)
from openpype_modules.ftrack.lib import get_ftrack_event_mongo_info
from openpype.lib import OpenPypeMongoConnection
from openpype.lib import (
OpenPypeMongoConnection,
get_openpype_version,
get_build_version
)
from openpype.api import Logger
log = Logger.get_logger("Event storer")
@ -153,9 +157,11 @@ def send_status(event):
new_event_data = {
"subprocess_id": os.environ["FTRACK_EVENT_SUB_ID"],
"source": "storer",
"status_info": {
"created_at": subprocess_started.strftime("%Y.%m.%d %H:%M:%S")
}
"status_info": [
["created_at", subprocess_started.strftime("%Y.%m.%d %H:%M:%S")],
["OpenPype version", get_openpype_version() or "N/A"],
["OpenPype build version", get_build_version() or "N/A"]
]
}
new_event = ftrack_api.event.base.Event(

View file

@ -25,7 +25,7 @@ class CredentialsDialog(QtWidgets.QDialog):
self._is_logged = False
self._in_advance_mode = False
icon = QtGui.QIcon(resources.pype_icon_filepath())
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(

View file

@ -40,10 +40,6 @@ class LogViewModule(OpenPypeModule, ITrayModule):
def tray_exit(self):
return
def connect_with_modules(self, _enabled_modules):
"""Nothing special."""
return
def _show_logs_gui(self):
if self.window:
self.window.show()

View file

@ -3,13 +3,10 @@ import json
import appdirs
import requests
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IWebServerRoutes
)
from openpype_interfaces import ITrayModule
class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
class MusterModule(OpenPypeModule, ITrayModule):
"""
Module handling Muster Render credentials. This will display dialog
asking for user credentials for Muster if not already specified.
@ -54,9 +51,6 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
"""Nothing special for Muster."""
return
def connect_with_modules(self, *_a, **_kw):
return
# Definition of Tray menu
def tray_menu(self, parent):
"""Add **change credentials** option to tray menu."""
@ -76,13 +70,6 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
parent.addMenu(menu)
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import MusterModuleRestApi
self.rest_api_obj = MusterModuleRestApi(self, server_manager)
def load_credentials(self):
"""
Get credentials from JSON file
@ -142,6 +129,14 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
if self.widget_login:
self.widget_login.show()
# Webserver module implementation
def webserver_initialization(self, server_manager):
"""Add routes for Muster login."""
if self.tray_initialized:
from .rest_api import MusterModuleRestApi
self.rest_api_obj = MusterModuleRestApi(self, server_manager)
def _requests_post(self, *args, **kwargs):
""" Wrapper for requests, disabling SSL certificate validation if
DONT_VERIFY_SSL environment variable is found. This is useful when

View file

@ -17,7 +17,7 @@ class MusterLogin(QtWidgets.QWidget):
self.module = module
# Icon
icon = QtGui.QIcon(resources.pype_icon_filepath())
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(

View file

@ -17,9 +17,6 @@ class ProjectManagerAction(OpenPypeModule, ITrayAction):
# Tray attributes
self.project_manager_window = None
def connect_with_modules(self, *_a, **_kw):
return
def tray_init(self):
"""Initialization in tray implementation of ITrayAction."""
self.create_project_manager_window()

View file

@ -18,9 +18,6 @@ class PythonInterpreterAction(OpenPypeModule, ITrayAction):
if self._interpreter_window is not None:
self._interpreter_window.save_registry()
def connect_with_modules(self, *args, **kwargs):
pass
def create_interpreter_window(self):
"""Initializa Settings Qt window."""
if self._interpreter_window:

View file

@ -331,7 +331,7 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
super(PythonInterpreterWidget, self).__init__(parent)
self.setWindowTitle("OpenPype Console")
self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath()))
self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath()))
self.ansi_escape = re.compile(
r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]"
@ -387,8 +387,6 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
self.setStyleSheet(load_stylesheet())
self.resize(self.default_width, self.default_height)
self._init_from_registry()
if self._tab_widget.count() < 1:
@ -396,16 +394,23 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
def _init_from_registry(self):
setting_registry = PythonInterpreterRegistry()
width = None
height = None
try:
width = setting_registry.get_item("width")
height = setting_registry.get_item("height")
if width is not None and height is not None:
self.resize(width, height)
except ValueError:
pass
if width is None or width < 200:
width = self.default_width
if height is None or height < 200:
height = self.default_height
self.resize(width, height)
try:
sizes = setting_registry.get_item("splitter_sizes")
if len(sizes) == len(self._widgets_splitter.sizes()):

View file

@ -19,9 +19,6 @@ class SettingsAction(OpenPypeModule, ITrayAction):
# Tray attributes
self.settings_window = None
def connect_with_modules(self, *_a, **_kw):
return
def tray_init(self):
"""Initialization in tray implementation of ITrayAction."""
self.create_settings_window()
@ -84,9 +81,6 @@ class LocalSettingsAction(OpenPypeModule, ITrayAction):
self.settings_window = None
self._first_trigger = True
def connect_with_modules(self, *_a, **_kw):
return
def tray_init(self):
"""Initialization in tray implementation of ITrayAction."""
self.create_settings_window()

View file

@ -17,10 +17,6 @@ class SlackIntegrationModule(OpenPypeModule, IPluginPaths, ILaunchHookPaths):
slack_settings = modules_settings[self.name]
self.enabled = slack_settings["enabled"]
def connect_with_modules(self, _enabled_modules):
"""Nothing special."""
return
def get_launch_hook_paths(self):
"""Implementation of `ILaunchHookPaths`."""
return os.path.join(SLACK_MODULE_DIR, "launch_hooks")

View file

@ -29,13 +29,35 @@ class AbstractProvider:
@classmethod
@abc.abstractmethod
def get_configurable_items(cls):
def get_system_settings_schema(cls):
"""
Returns filtered dict of editable properties
Returns dict for editable properties on system settings level
Returns:
(dict)
(list) of dict
"""
@classmethod
@abc.abstractmethod
def get_project_settings_schema(cls):
"""
Returns dict for editable properties on project settings level
Returns:
(list) of dict
"""
@classmethod
@abc.abstractmethod
def get_local_settings_schema(cls):
"""
Returns dict for editable properties on local settings level
Returns:
(list) of dict
"""
@abc.abstractmethod

View file

@ -8,7 +8,7 @@ import platform
from openpype.api import Logger
from openpype.api import get_system_settings
from .abstract_provider import AbstractProvider
from ..utils import time_function, ResumableError, EditableScopes
from ..utils import time_function, ResumableError
log = Logger().get_logger("SyncServer")
@ -96,30 +96,61 @@ class GDriveHandler(AbstractProvider):
return self.service is not None
@classmethod
def get_configurable_items(cls):
def get_system_settings_schema(cls):
"""
Returns filtered dict of editable properties.
Returns dict for editable properties on system settings level
Returns:
(list) of dict
"""
return []
@classmethod
def get_project_settings_schema(cls):
"""
Returns dict for editable properties on project settings level
Returns:
(list) of dict
"""
# {platform} tells that value is multiplatform and only specific OS
# should be returned
editable = [
# credentials could be overriden on Project or User level
{
'key': "credentials_url",
'label': "Credentials url",
'type': 'text'
},
# roots could be overriden only on Project leve, User cannot
{
'key': "roots",
'label': "Roots",
'type': 'dict'
}
]
return editable
@classmethod
def get_local_settings_schema(cls):
"""
Returns dict for editable properties on local settings level
Returns:
(dict)
"""
# {platform} tells that value is multiplatform and only specific OS
# should be returned
editable = {
editable = [
# credentials could be override on Project or User level
'credentials_url': {
'scope': [EditableScopes.PROJECT,
EditableScopes.LOCAL],
{
'key': "credentials_url",
'label': "Credentials url",
'type': 'text',
'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501
},
# roots could be override only on Project leve, User cannot
'root': {'scope': [EditableScopes.PROJECT],
'label': "Roots",
'type': 'dict'}
}
}
]
return editable
def get_roots_config(self, anatomy=None):

View file

@ -76,6 +76,14 @@ class ProviderFactory:
return provider_info[0].get_configurable_items()
def get_provider_cls(self, provider_code):
"""
Returns class object for 'provider_code' to run class methods on.
"""
provider_info = self._get_creator_info(provider_code)
return provider_info[0]
def _get_creator_info(self, provider):
"""
Collect all necessary info for provider. Currently only creator

View file

@ -7,8 +7,6 @@ import time
from openpype.api import Logger, Anatomy
from .abstract_provider import AbstractProvider
from ..utils import EditableScopes
log = Logger().get_logger("SyncServer")
@ -30,18 +28,51 @@ class LocalDriveHandler(AbstractProvider):
return True
@classmethod
def get_configurable_items(cls):
def get_system_settings_schema(cls):
"""
Returns filtered dict of editable properties
Returns dict for editable properties on system settings level
Returns:
(list) of dict
"""
return []
@classmethod
def get_project_settings_schema(cls):
"""
Returns dict for editable properties on project settings level
Returns:
(list) of dict
"""
# for non 'studio' sites, 'studio' is configured in Anatomy
editable = [
{
'key': "roots",
'label': "Roots",
'type': 'dict'
}
]
return editable
@classmethod
def get_local_settings_schema(cls):
"""
Returns dict for editable properties on local settings level
Returns:
(dict)
"""
editable = {
'root': {'scope': [EditableScopes.LOCAL],
'label': "Roots",
'type': 'dict'}
}
editable = [
{
'key': "roots",
'label': "Roots",
'type': 'dict'
}
]
return editable
def upload_file(self, source_path, target_path,

View file

@ -16,14 +16,13 @@ from openpype.api import (
get_local_site_id)
from openpype.lib import PypeLogger
from openpype.settings.lib import (
get_default_project_settings,
get_default_anatomy_settings,
get_anatomy_settings)
from .providers.local_drive import LocalDriveHandler
from .providers import lib
from .utils import time_function, SyncStatus, EditableScopes
from .utils import time_function, SyncStatus
log = PypeLogger().get_logger("SyncServer")
@ -399,204 +398,239 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
return remote_site
def get_local_settings_schema(self):
"""Wrapper for Local settings - all projects incl. Default"""
return self.get_configurable_items(EditableScopes.LOCAL)
# Methods for Settings UI to draw appropriate forms
@classmethod
def get_system_settings_schema(cls):
""" Gets system level schema of configurable items
def get_configurable_items(self, scope=None):
Used for Setting UI to provide forms.
"""
Returns list of sites that could be configurable for all projects.
ret_dict = {}
for provider_code in lib.factory.providers:
ret_dict[provider_code] = \
lib.factory.get_provider_cls(provider_code). \
get_system_settings_schema()
Could be filtered by 'scope' argument (list)
return ret_dict
Args:
scope (list of utils.EditableScope)
@classmethod
def get_project_settings_schema(cls):
""" Gets project level schema of configurable items.
Returns:
(dict of list of dict)
{
siteA : [
{
key:"root", label:"root",
"value":"{'work': 'c:/projects'}",
"type": "dict",
"children":[
{ "key": "work",
"type": "text",
"value": "c:/projects"}
]
},
{
key:"credentials_url", label:"Credentials url",
"value":"'c:/projects/cred.json'", "type": "text",
"namespace": "{project_setting}/global/sync_server/
sites"
}
]
}
It is not using Setting! Used for Setting UI to provide forms.
"""
editable = {}
applicable_projects = list(self.connection.projects())
applicable_projects.append(None)
for project in applicable_projects:
project_name = None
if project:
project_name = project["name"]
ret_dict = {}
for provider_code in lib.factory.providers:
ret_dict[provider_code] = \
lib.factory.get_provider_cls(provider_code). \
get_project_settings_schema()
items = self.get_configurable_items_for_project(project_name,
scope)
editable.update(items)
return ret_dict
return editable
@classmethod
def get_local_settings_schema(cls):
""" Gets local level schema of configurable items.
def get_local_settings_schema_for_project(self, project_name):
"""Wrapper for Local settings - for specific 'project_name'"""
return self.get_configurable_items_for_project(project_name,
EditableScopes.LOCAL)
def get_configurable_items_for_project(self, project_name=None,
scope=None):
It is not using Setting! Used for Setting UI to provide forms.
"""
Returns list of items that could be configurable for specific
'project_name'
ret_dict = {}
for provider_code in lib.factory.providers:
ret_dict[provider_code] = \
lib.factory.get_provider_cls(provider_code). \
get_local_settings_schema()
Args:
project_name (str) - None > default project,
scope (list of utils.EditableScope)
(optional, None is all scopes, default is LOCAL)
return ret_dict
Returns:
(dict of list of dict)
{
siteA : [
{
key:"root", label:"root",
"type": "dict",
"children":[
{ "key": "work",
"type": "text",
"value": "c:/projects"}
]
},
{
key:"credentials_url", label:"Credentials url",
"value":"'c:/projects/cred.json'", "type": "text",
"namespace": "{project_setting}/global/sync_server/
sites"
}
]
}
"""
allowed_sites = set()
sites = self.get_all_site_configs(project_name)
if project_name:
# Local Settings can select only from allowed sites for project
allowed_sites.update(set(self.get_active_sites(project_name)))
allowed_sites.update(set(self.get_remote_sites(project_name)))
editable = {}
for site_name in sites.keys():
if allowed_sites and site_name not in allowed_sites:
continue
items = self.get_configurable_items_for_site(project_name,
site_name,
scope)
# Local Settings need 'local' instead of real value
site_name = site_name.replace(get_local_site_id(), 'local')
editable[site_name] = items
return editable
def get_local_settings_schema_for_site(self, project_name, site_name):
"""Wrapper for Local settings - for particular 'site_name and proj."""
return self.get_configurable_items_for_site(project_name,
site_name,
EditableScopes.LOCAL)
def get_configurable_items_for_site(self, project_name=None,
site_name=None,
scope=None):
"""
Returns list of items that could be configurable.
Args:
project_name (str) - None > default project
site_name (str)
scope (list of utils.EditableScope)
(optional, None is all scopes)
Returns:
(list)
[
{
key:"root", label:"root", type:"dict",
"children":[
{ "key": "work",
"type": "text",
"value": "c:/projects"}
]
}, ...
]
"""
provider_name = self.get_provider_for_site(site=site_name)
items = lib.factory.get_provider_configurable_items(provider_name)
if project_name:
sync_s = self.get_sync_project_setting(project_name,
exclude_locals=True,
cached=False)
else:
sync_s = get_default_project_settings(exclude_locals=True)
sync_s = sync_s["global"]["sync_server"]
sync_s["sites"].update(
self._get_default_site_configs(self.enabled))
editable = []
if type(scope) is not list:
scope = [scope]
scope = set(scope)
for key, properties in items.items():
if scope is None or scope.intersection(set(properties["scope"])):
val = sync_s.get("sites", {}).get(site_name, {}).get(key)
item = {
"key": key,
"label": properties["label"],
"type": properties["type"]
}
if properties.get("namespace"):
item["namespace"] = properties.get("namespace")
if "platform" in item["namespace"]:
try:
if val:
val = val[platform.system().lower()]
except KeyError:
st = "{}'s field value {} should be".format(key, val) # noqa: E501
log.error(st + " multiplatform dict")
item["namespace"] = item["namespace"].replace('{site}',
site_name)
children = []
if properties["type"] == "dict":
if val:
for val_key, val_val in val.items():
child = {
"type": "text",
"key": val_key,
"value": val_val
}
children.append(child)
if properties["type"] == "dict":
item["children"] = children
else:
item["value"] = val
editable.append(item)
return editable
# Needs to be refactored after Settings are updated
# # Methods for Settings to get appriate values to fill forms
# def get_configurable_items(self, scope=None):
# """
# Returns list of sites that could be configurable for all projects
#
# Could be filtered by 'scope' argument (list)
#
# Args:
# scope (list of utils.EditableScope)
#
# Returns:
# (dict of list of dict)
# {
# siteA : [
# {
# key:"root", label:"root",
# "value":"{'work': 'c:/projects'}",
# "type": "dict",
# "children":[
# { "key": "work",
# "type": "text",
# "value": "c:/projects"}
# ]
# },
# {
# key:"credentials_url", label:"Credentials url",
# "value":"'c:/projects/cred.json'", "type": "text", # noqa: E501
# "namespace": "{project_setting}/global/sync_server/ # noqa: E501
# sites"
# }
# ]
# }
# """
# editable = {}
# applicable_projects = list(self.connection.projects())
# applicable_projects.append(None)
# for project in applicable_projects:
# project_name = None
# if project:
# project_name = project["name"]
#
# items = self.get_configurable_items_for_project(project_name,
# scope)
# editable.update(items)
#
# return editable
#
# def get_local_settings_schema_for_project(self, project_name):
# """Wrapper for Local settings - for specific 'project_name'"""
# return self.get_configurable_items_for_project(project_name,
# EditableScopes.LOCAL)
#
# def get_configurable_items_for_project(self, project_name=None,
# scope=None):
# """
# Returns list of items that could be configurable for specific
# 'project_name'
#
# Args:
# project_name (str) - None > default project,
# scope (list of utils.EditableScope)
# (optional, None is all scopes, default is LOCAL)
#
# Returns:
# (dict of list of dict)
# {
# siteA : [
# {
# key:"root", label:"root",
# "type": "dict",
# "children":[
# { "key": "work",
# "type": "text",
# "value": "c:/projects"}
# ]
# },
# {
# key:"credentials_url", label:"Credentials url",
# "value":"'c:/projects/cred.json'", "type": "text",
# "namespace": "{project_setting}/global/sync_server/
# sites"
# }
# ]
# }
# """
# allowed_sites = set()
# sites = self.get_all_site_configs(project_name)
# if project_name:
# # Local Settings can select only from allowed sites for project
# allowed_sites.update(set(self.get_active_sites(project_name)))
# allowed_sites.update(set(self.get_remote_sites(project_name)))
#
# editable = {}
# for site_name in sites.keys():
# if allowed_sites and site_name not in allowed_sites:
# continue
#
# items = self.get_configurable_items_for_site(project_name,
# site_name,
# scope)
# # Local Settings need 'local' instead of real value
# site_name = site_name.replace(get_local_site_id(), 'local')
# editable[site_name] = items
#
# return editable
#
# def get_configurable_items_for_site(self, project_name=None,
# site_name=None,
# scope=None):
# """
# Returns list of items that could be configurable.
#
# Args:
# project_name (str) - None > default project
# site_name (str)
# scope (list of utils.EditableScope)
# (optional, None is all scopes)
#
# Returns:
# (list)
# [
# {
# key:"root", label:"root", type:"dict",
# "children":[
# { "key": "work",
# "type": "text",
# "value": "c:/projects"}
# ]
# }, ...
# ]
# """
# provider_name = self.get_provider_for_site(site=site_name)
# items = lib.factory.get_provider_configurable_items(provider_name)
#
# if project_name:
# sync_s = self.get_sync_project_setting(project_name,
# exclude_locals=True,
# cached=False)
# else:
# sync_s = get_default_project_settings(exclude_locals=True)
# sync_s = sync_s["global"]["sync_server"]
# sync_s["sites"].update(
# self._get_default_site_configs(self.enabled))
#
# editable = []
# if type(scope) is not list:
# scope = [scope]
# scope = set(scope)
# for key, properties in items.items():
# if scope is None or scope.intersection(set(properties["scope"])):
# val = sync_s.get("sites", {}).get(site_name, {}).get(key)
#
# item = {
# "key": key,
# "label": properties["label"],
# "type": properties["type"]
# }
#
# if properties.get("namespace"):
# item["namespace"] = properties.get("namespace")
# if "platform" in item["namespace"]:
# try:
# if val:
# val = val[platform.system().lower()]
# except KeyError:
# st = "{}'s field value {} should be".format(key, val) # noqa: E501
# log.error(st + " multiplatform dict")
#
# item["namespace"] = item["namespace"].replace('{site}',
# site_name)
# children = []
# if properties["type"] == "dict":
# if val:
# for val_key, val_val in val.items():
# child = {
# "type": "text",
# "key": val_key,
# "value": val_val
# }
# children.append(child)
#
# if properties["type"] == "dict":
# item["children"] = children
# else:
# item["value"] = val
#
# editable.append(item)
#
# return editable
def reset_timer(self):
"""
@ -611,7 +645,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
enabled_projects = []
if self.enabled:
for project in self.connection.projects():
for project in self.connection.projects(projection={"name": 1}):
project_name = project["name"]
project_settings = self.get_sync_project_setting(project_name)
if project_settings and project_settings.get("enabled"):
@ -646,9 +680,6 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
return sites
def connect_with_modules(self, *_a, **kw):
return
def tray_init(self):
"""
Actual initialization of Sync Server.
@ -781,17 +812,22 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
def _prepare_sync_project_settings(self, exclude_locals):
sync_project_settings = {}
system_sites = self.get_all_site_configs()
for collection in self.connection.database.collection_names(False):
project_docs = self.connection.projects(
projection={"name": 1},
only_active=True
)
for project_doc in project_docs:
project_name = project_doc["name"]
sites = copy.deepcopy(system_sites) # get all configured sites
proj_settings = self._parse_sync_settings_from_settings(
get_project_settings(collection,
get_project_settings(project_name,
exclude_locals=exclude_locals))
sites.update(self._get_default_site_configs(
proj_settings["enabled"], collection))
proj_settings["enabled"], project_name))
sites.update(proj_settings['sites'])
proj_settings["sites"] = sites
sync_project_settings[collection] = proj_settings
sync_project_settings[project_name] = proj_settings
if not sync_project_settings:
log.info("No enabled and configured projects for sync.")
return sync_project_settings

View file

@ -26,7 +26,7 @@ class SyncServerWindow(QtWidgets.QDialog):
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setStyleSheet(style.load_stylesheet())
self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath()))
self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath()))
self.resize(1450, 700)
self.timer = QtCore.QTimer()
@ -77,19 +77,36 @@ class SyncServerWindow(QtWidgets.QDialog):
self.setWindowTitle("Sync Queue")
self.projects.project_changed.connect(
lambda: repres.table_view.model().set_project(
self.projects.current_project))
self._on_project_change
)
self.pause_btn.clicked.connect(self._pause)
self.pause_btn.setAutoDefault(False)
self.pause_btn.setDefault(False)
repres.message_generated.connect(self._update_message)
self.projects.message_generated.connect(self._update_message)
self.representationWidget = repres
def _on_project_change(self):
if self.projects.current_project is None:
return
self.representationWidget.table_view.model().set_project(
self.projects.current_project
)
project_name = self.projects.current_project
if not self.sync_server.get_sync_project_setting(project_name):
self.projects.message_generated.emit(
"Project {} not active anymore".format(project_name))
self.projects.refresh()
return
def showEvent(self, event):
self.representationWidget.model.set_project(
self.projects.current_project)
self.projects.refresh()
self._set_running(True)
super().showEvent(event)

View file

@ -5,7 +5,7 @@ from bson.objectid import ObjectId
from Qt import QtCore
from Qt.QtCore import Qt
from avalon.tools.delegates import pretty_timestamp
from openpype.tools.utils.delegates import pretty_timestamp
from avalon.vendor import qtawesome
from openpype.lib import PypeLogger
@ -17,25 +17,6 @@ from . import lib
log = PypeLogger().get_logger("SyncServer")
class ProjectModel(QtCore.QAbstractListModel):
def __init__(self, *args, projects=None, **kwargs):
super(ProjectModel, self).__init__(*args, **kwargs)
self.projects = projects or []
def data(self, index, role):
if role == Qt.DisplayRole:
# See below for the data structure.
status, text = self.projects[index.row()]
# Return the todo text only.
return text
def rowCount(self, _index):
return len(self.todos)
def columnCount(self, _index):
return len(self._header)
class _SyncRepresentationModel(QtCore.QAbstractTableModel):
COLUMN_LABELS = []
@ -320,6 +301,10 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
"""
self._project = project
self.sync_server.set_sync_project_settings()
# project might have been deactivated in the meantime
if not self.sync_server.get_sync_project_setting(project):
return
self.active_site = self.sync_server.get_active_site(self.project)
self.remote_site = self.sync_server.get_remote_site(self.project)
self.refresh()

Some files were not shown because too many files have changed in this diff Show more